Skip to main content

Overview

This page provides an in-depth look at each microservice in the Pricing Intelligence platform, including implementation details, APIs, and internal architecture.

Harvey API

Harvey Agent

Purpose

Harvey (Holistic Analysis and Regulation Virtual Expert for You) is the AI agent that orchestrates the entire pricing analysis workflow. It implements the ReAct pattern (Reasoning + Acting) to answer natural language queries about SaaS pricing.

Technology Stack

  • Language: Python 3.11+
  • Framework: FastAPI
  • AI Model: OpenAI GPT (configurable via OPENAI_MODEL env var)
  • Protocol: MCP (Model Context Protocol)

Key Components

1. Agent Core (harvey_api/src/harvey_api/agent.py)

The HarveyAgent class implements the ReAct loop:
class HarveyAgent:
    async def handle_question(
        self,
        question: str,
        pricing_urls: Optional[List[str]] = None,
        yaml_contents: Optional[List[str]] = None,
    ) -> Dict[str, Any]:
        # 1. Generate plan using LLM
        plan = await self._generate_plan(
            question, pricing_urls, yaml_alias_map
        )
        
        # 2. Validate requirements
        self._validate_yaml_requirement(plan, yaml_contents)
        
        # 3. Execute actions via MCP
        results, last_payload = await self._execute_actions(
            actions, default_reference, available_urls, objective, yaml_alias_map
        )
        
        # 4. Generate natural language answer
        answer = await self._generate_answer(
            question, plan, payload, yaml_alias_map
        )
        
        return {"plan": plan, "result": result_payload, "answer": answer}

2. Planning System

Harvey uses a two-stage LLM approach:
1

Planning Phase

The LLM analyzes the user’s question and available context (URLs, uploaded YAMLs) to generate a structured execution plan:
{
  "actions": [
    "iPricing",
    {"name": "optimal", "objective": "minimize", "filters": {"features": ["SSO"]}}
  ],
  "requires_uploaded_yaml": false,
  "use_pricing2yaml_spec": false
}
2

Execution Phase

Harvey executes each action sequentially via MCP tools, collecting results
3

Answer Phase

The LLM synthesizes tool results into a natural language answer, grounded in the actual data

3. Grounding Mechanism

Harvey implements context-aware grounding to prevent hallucinations:
# From harvey_api/src/harvey_api/agent.py:73-102
DEFAULT_PLAN_PROMPT = """You are H.A.R.V.E.Y., an expert AI agent designed to reason about pricing models using the ReAct pattern.

## Context Awareness & Grounding

You have full access to the uploaded Pricing2Yaml (iPricing) files.
**CRITICAL:** You MUST analyze the YAML content to identify the exact keys used for features (`feature.name`) and usage limits (`usageLimit.name`) **before** constructing any filters.

### Per-Context Grounding

When handling multiple pricings (e.g., comparing SaaS A vs. SaaS B), you MUST generate **separate actions** for each pricing source/URL.

* Filters for SaaS A must use **SaaS A's** exact feature/limit names.
* Filters for SaaS B must use **SaaS B's** exact feature/limit names.
"""
This approach ensures that feature names like “SSO” vs “Single Sign-On” are correctly mapped to the actual YAML schema before constructing filters.

4. MCP Client Integration

Harvey communicates with the MCP Server via the official MCP Python SDK:
# harvey_api/src/harvey_api/clients/mcp.py
class MCPWorkflowClient:
    async def run_optimal(
        self,
        url: str,
        filters: Optional[Dict[str, Any]],
        solver: str,
        objective: str,
        refresh: bool,
        yaml_content: Optional[str],
    ) -> Dict[str, Any]:
        result = await self._call_tool(
            "optimal",
            {
                "pricing_url": url,
                "pricing_yaml": yaml_content,
                "filters": filters,
                "solver": solver,
                "objective": objective,
                "refresh": refresh,
            },
        )
        return result

API Endpoints

POST /chat

Main conversational endpoint for pricing queries.
curl -X POST http://localhost:8086/chat \
  -H "Content-Type: application/json" \
  -d '{
    "message": "What is the cheapest plan for Buffer with 10 channels?",
    "pricing_urls": ["https://buffer.com/pricing"]
  }'
Response:
{
  "plan": {
    "actions": ["iPricing", {"name": "optimal", "objective": "minimize"}],
    "requires_uploaded_yaml": false,
    "use_pricing2yaml_spec": false
  },
  "result": {
    "optimal": {
      "subscription": {"plan": "Team", "addOns": []},
      "cost": 10.0
    }
  },
  "answer": "The cheapest plan for Buffer that includes 10 channels is the **Team plan** at $10/month."
}

POST /upload

Upload a Pricing2Yaml file for analysis.
curl -F '[email protected];type=application/yaml' \
  http://localhost:8086/upload
Response:
{
  "filename": "hubspot-pricing.yaml",
  "relative_path": "/static/hubspot-pricing.yaml"
}

DELETE /pricing/{filename}

Delete an uploaded YAML file.
curl -X DELETE http://localhost:8086/pricing/hubspot-pricing.yaml

Configuration

Environment VariableDefaultDescription
OPENAI_API_KEYrequiredOpenAI API key for LLM access
OPENAI_MODELgpt-5-nanoOpenAI model to use
MCP_SERVER_URLhttp://mcp-server:8085/sseMCP Server endpoint
LOG_LEVELINFOLogging level

MCP Server

Purpose

The MCP Server implements the Model Context Protocol specification, exposing standardized tools and resources to AI agents. It acts as a bridge between Harvey and the backend services (A-MINT and Analysis API).

Technology Stack

  • Language: Python 3.11+
  • Framework: FastMCP (official MCP library)
  • Transport: Server-Sent Events (SSE) or stdio
  • Cache: In-memory or Redis

MCP Tools

The server exposes five core tools:

1. iPricing - Pricing Data Extraction

# mcp_server/src/pricing_mcp/mcp_server.py:193-223
@mcp.tool(name="iPricing")
async def ipricing(
    pricing_url: Optional[str] = None,
    pricing_yaml: Optional[str] = None,
    refresh: bool = False,
) -> Dict[str, Any]:
    """Return the canonical Pricing2Yaml (iPricing) document."""
    result = await container.workflow.get_ipricing(
        url=pricing_url,
        yaml_content=pricing_yaml,
        refresh=refresh,
    )
    return result
Example Usage:
{
  "tool": "iPricing",
  "arguments": {
    "pricing_url": "https://buffer.com/pricing"
  }
}
Returns:
{
  "request": {"url": "https://buffer.com/pricing", "refresh": false},
  "pricing_yaml": "saasName: Buffer\nversion: 1.0\nplans:\n  - name: Free\n...",
  "source": "amint"
}

2. summary - Pricing Statistics

@mcp.tool()
async def summary(
    pricing_url: Optional[str] = None,
    pricing_yaml: Optional[str] = None,
    refresh: bool = False,
) -> Dict[str, Any]:
    """Return contextual pricing summary data."""
    result = await container.workflow.run_summary(
        url=pricing_url,
        yaml_content=pricing_yaml,
        refresh=refresh,
    )
    return result
Returns:
{
  "summary": {
    "numberOfPlans": 3,
    "numberOfFeatures": 15,
    "numberOfAddOns": 5,
    "minPlanPrice": 10.0,
    "maxPlanPrice": 100.0
  }
}

3. subscriptions - Configuration Enumeration

@mcp.tool()
async def subscriptions(
    pricing_url: Optional[str] = None,
    pricing_yaml: Optional[str] = None,
    filters: Optional[Dict[str, Any]] = None,
    solver: str = "minizinc",
    refresh: bool = False,
) -> Dict[str, Any]:
    """Enumerate subscriptions within the pricing configuration space."""
    result = await container.workflow.run_subscriptions(
        url=pricing_url or "",
        filters=filters,
        solver=solver,
        refresh=refresh,
        yaml_content=pricing_yaml,
    )
    return result
Filter Schema:
interface FilterCriteria {
  minPrice?: number;
  maxPrice?: number;
  maxSubscriptionSize?: number;
  features?: string[];
  usageLimits?: Record<string, number>;
}

4. optimal - Best Configuration

@mcp.tool()
async def optimal(
    pricing_url: Optional[str] = None,
    pricing_yaml: Optional[str] = None,
    filters: Optional[Dict[str, Any]] = None,
    solver: str = "minizinc",
    objective: str = "minimize",
    refresh: bool = False,
) -> Dict[str, Any]:
    """Compute the optimal subscription under the provided constraints."""
    result = await container.workflow.run_optimal(
        url=pricing_url or "",
        filters=filters,
        solver=solver,
        objective=objective,
        refresh=refresh,
        yaml_content=pricing_yaml,
    )
    return result
Returns:
{
  "result": {
    "optimal": {
      "subscription": {
        "plan": "Pro",
        "addOns": ["Advanced Analytics"]
      },
      "cost": 49.0,
      "cardinality": 1
    }
  }
}

5. validate - Pricing Model Validation

@mcp.tool()
async def validate(
    pricing_url: Optional[str] = None,
    pricing_yaml: Optional[str] = None,
    solver: str = "minizinc",
    refresh: bool = False,
) -> Dict[str, Any]:
    """Validate the pricing configuration against the selected solver."""
    result = await container.workflow.run_validation(
        url=pricing_url,
        solver=solver,
        refresh=refresh,
        yaml_content=pricing_yaml,
    )
    return result

MCP Resources

The server exposes one static resource:
@mcp.resource("resource://pricing/specification")
async def pricing2yaml_specification() -> str:
    """Expose the Pricing2Yaml specification excerpt as a reusable resource."""
    return _PRICING2YAML_SPEC
This allows Harvey to query the Pricing2Yaml schema when validating or explaining syntax.

Workflow Orchestration

The PricingWorkflow class orchestrates calls to A-MINT and Analysis APIs:
# mcp_server/src/pricing_mcp/workflows/pricing.py:15-44
class PricingWorkflow:
    async def ensure_pricing_yaml(self, url: str, refresh: bool = False) -> str:
        # Check cache first
        cache_key = f"pricing:{url}"
        if not refresh:
            cached = await self._cache.get(cache_key)
            if cached:
                return cached
        
        # Call A-MINT to extract pricing
        yaml_content = await self._amint.transform(TransformOptions(url=url))
        
        # Cache result
        await self._cache.set(cache_key, yaml_content, ttl_seconds=self._cache_ttl)
        return yaml_content

Analysis API

Purpose

The Analysis API is the core business logic layer that handles pricing validation, optimization, and statistics using constraint programming.

Technology Stack

  • Language: TypeScript/Node.js 18+
  • Framework: Express.js
  • Solver Interface: MiniZinc (via shell execution)
  • Secondary Solver: Choco (via CSP Service REST API)

Architecture

┌─────────────────────────────────────────────────────────────┐
│                    Analysis API                            │
├─────────────────────────────────────────────────────────────┤
│  Express.js + TypeScript                                   │
│  ├─ REST Endpoints                                          │
│  ├─ Validation middleware                                   │
│  ├─ Multer for file upload                                  │
│  └─ CORS and error handling                                 │
├─────────────────────────────────────────────────────────────┤
│  Business Services                                         │
│  ├─ MinizincService                                         │
│  ├─ JobManager                                              │
│  ├─ PricingValidator                                        │
│  └─ AnalyticsProcessor                                      │
├─────────────────────────────────────────────────────────────┤
│  MiniZinc Engine                                           │
│  ├─ CSP models in .mzn                                      │
│  ├─ YAML → DZN conversion                                   │
│  ├─ Solver execution                                        │
│  └─ Result post-processing                                  │
└─────────────────────────────────────────────────────────────┘

API Endpoints

POST /api/v1/pricing/summary

Returns statistical summary of a pricing model.
curl -X POST http://localhost:8002/api/v1/pricing/summary \
  -F "[email protected]"
Response:
{
  "numberOfPlans": 3,
  "numberOfFeatures": 15,
  "numberOfAddOns": 5,
  "minPlanPrice": 10.0,
  "maxPlanPrice": 100.0,
  "hasFreePlan": true,
  "currency": "USD"
}

POST /api/v1/pricing/analysis

Starts an asynchronous analysis job.
curl -X POST http://localhost:8002/api/v1/pricing/analysis \
  -F "[email protected]" \
  -F "operation=optimal" \
  -F "solver=minizinc" \
  -F "objective=minimize" \
  -F 'filters={"features": ["SSO"], "maxPrice": 50}'
Response:
{
  "jobId": "job_12345",
  "status": "PENDING"
}

GET /api/v1/pricing/analysis/{jobId}

Retrieves job status or results.
curl http://localhost:8002/api/v1/pricing/analysis/job_12345
Response (completed):
{
  "jobId": "job_12345",
  "status": "COMPLETED",
  "results": {
    "optimal": {
      "subscription": {"plan": "Pro", "addOns": []},
      "cost": 29.0
    }
  }
}

Operations

Verifies mathematical and logical coherence:
  • Price consistency between plans
  • Feature coherence between plans
  • Add-on dependency validation
  • Detection of impossible configurations
Finds optimal configuration based on criteria:
  • minimize: Cheapest plan satisfying filters
  • maximize: Most expensive configuration
  • Returns single best result with cost breakdown
Lists all valid subscription combinations:
  • Complete list of plan + add-on configurations
  • Cost of each configuration
  • Total cardinality (count)
Same as subscriptions but with filter constraints applied

Solver Integration

The Analysis API supports two CSP solvers:

MiniZinc (Default)

Direct integration via shell execution:
// analysis_api/src/services/minizinc.service.ts
const result = await execAsync(
  `minizinc --solver chuffed -a ${modelFile} ${dataFile}`
);

Choco (via CSP Service)

RESTful integration with Java-based Choco solver:
const response = await axios.post(
  `${CHOCO_API}/solve`,
  { yaml: pricingYaml, operation, filters }
);

A-MINT API

Purpose

A-MINT (AI-powered Model INTelligence) extracts structured pricing data from unstructured sources (web pages) and converts them to the Pricing2Yaml format.

Technology Stack

  • Language: Python 3.11+
  • Framework: FastAPI
  • AI Model: OpenAI GPT-4 (vision-capable for screenshots)
  • Scraping: BeautifulSoup, Playwright

How It Works

1

Fetch Pricing Page

Downloads HTML content from the provided URL, optionally taking screenshots
2

Extract Structure

Uses LLM to identify plans, features, prices, and limits from the raw HTML/screenshots
3

Generate Pricing2Yaml

Converts extracted data into standardized YAML format with schema validation
4

Validate & Return

Validates YAML against Pricing2Yaml schema before returning

API Endpoint

POST /transform

curl -X POST http://localhost:8001/transform \
  -H "Content-Type: application/json" \
  -d '{"url": "https://buffer.com/pricing"}'
Response:
saasName: Buffer
version: 1.0
day: 30
year: 365
currency: USD

plans:
  - name: Free
    price: 0
    features:
      - channels: {min: 3, max: 3}
      - analytics: basic
  
  - name: Team
    price: 10
    features:
      - channels: {min: 10, max: 10}
      - analytics: advanced
      - collaboration: true

Configuration

Environment VariableDefaultDescription
OPENAI_API_KEYrequiredOpenAI API key for extraction
ANALYSIS_APIhttp://analysis-api:3000/api/v1Analysis API endpoint
LOG_LEVELINFOLogging level

CSP Service (Choco)

Purpose

The CSP Service wraps the Choco constraint solver, providing RESTful access to Java-based constraint satisfaction solving.

Technology Stack

  • Language: Java 17
  • Framework: Spring Boot
  • Solver: Choco Solver 4.x

Key Components

1. YAML to CSP Converter

// csp/src/main/java/org/isa/pricing/csp/parser/Yaml2CSP.java
public class Yaml2CSP {
    public Model convertToModel(String yamlContent) {
        // Parse YAML
        PricingModel pricing = parseYaml(yamlContent);
        
        // Create Choco model
        Model model = new Model("Pricing CSP");
        
        // Define variables (plan selection, add-ons)
        IntVar planVar = model.intVar("plan", 0, pricing.plans.size() - 1);
        BoolVar[] addonVars = model.boolVarArray("addons", pricing.addOns.size());
        
        // Add constraints (features, limits, prices)
        addFeatureConstraints(model, planVar, addonVars, pricing);
        addPriceConstraints(model, planVar, addonVars, pricing);
        
        return model;
    }
}

2. CSP Controller

@RestController
@RequestMapping("/solve")
public class CSPController {
    @PostMapping
    public CSPOutput solve(@RequestBody SolveRequest request) {
        Model model = yaml2CSP.convertToModel(request.getYaml());
        
        // Apply filters
        applyFilters(model, request.getFilters());
        
        // Set objective
        if (request.getObjective().equals("minimize")) {
            model.setObjective(Model.MINIMIZE, costVar);
        }
        
        // Solve
        Solver solver = model.getSolver();
        Solution solution = solver.findOptimalSolution(costVar);
        
        return buildOutput(solution);
    }
}

API Endpoint

POST /solve

curl -X POST http://localhost:8000/solve \
  -H "Content-Type: application/json" \
  -d '{
    "yaml": "<pricing yaml content>",
    "operation": "optimal",
    "objective": "minimize",
    "filters": {"features": ["SSO"]}
  }'

Frontend

Purpose

React-based chat interface for interacting with Harvey.

Technology Stack

  • Framework: React 18 + Vite
  • Language: TypeScript
  • UI Library: Custom components
  • State Management: React Context

Key Features

  • Real-time Streaming: SSE connection to Harvey for streaming responses
  • File Upload: Drag-and-drop YAML file upload
  • Pricing Visualization: Renders pricing tables and comparisons
  • Chat History: Maintains conversation context

Next Steps

Architecture Overview

Return to architecture overview

Data Flow

Explore request/response patterns

Build docs developers (and LLMs) love