LegalDoc / ARCHITECTURE.md
PrernaKu's picture
Upload 13 files
e36160c verified
|
Raw History Blame Contribute Delete
21.9 kB

Multi-Agent Procurement System - Architecture Document

Executive Summary

This document provides a comprehensive architectural overview of the Multi-Agent Autonomous Procurement System. It is designed for architects, engineers, and technical leads evaluating this system for production deployment.

Key Design Principles:

  • Modularity: Each agent has a single, well-defined responsibility
  • Observability: Full tracing of all LLM calls and agent decisions
  • Human-in-the-Loop: Critical decisions subject to human approval
  • State Persistence: Complete workflow state maintained across interruptions
  • Production-Ready: Follows enterprise patterns for error handling and monitoring

System Overview

High-Level Architecture

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚                    MULTI-AGENT PROCUREMENT SYSTEM                       β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

External Inputs          Agents & Tools          State Management
    β”‚                         β”‚                        β”‚
    β”œβ”€ Procurement Request    β”œβ”€ ResearchAgent ───┐   β”œβ”€ ProcurementState
    β”œβ”€ Budget Limit           β”‚  └─ VendorSearch   β”‚   β”‚  β”œβ”€ vendor_options
    └─ (Human Approval)       β”‚                    β”‚   β”‚  β”œβ”€ selected_vendor
                              β”œβ”€ AnalysisAgent ─┐ β”‚   β”‚  β”œβ”€ analysis_approved
                              β”‚  β”œβ”€ LLM Eval     β”‚ β”‚   β”‚  β”œβ”€ human_approved
                              β”‚  └─ BudgetCheck  β”‚ β”‚   β”‚  └─ contract_draft
                              β”‚                  β”‚ β”‚   β”‚
                              β”œβ”€ ApprovalGate ───┼─┼──→ MemorySaver
                              β”‚  └─ (Interrupts)  β”‚ β”‚   β”‚  (Checkpointer)
                              β”‚                  β”‚ β”‚   β”‚
                              β”œβ”€ LegalAgent ──┐  β”‚ β”‚   β”‚
                              β”‚  └─ ContractGen  β”‚ β”‚   β”‚
                              β”‚                  β–Ό β–Ό   β”‚
                              └─────────────────────── β””
                                   Command Routing

Data Flow

Initial Request
    β”‚
    β–Ό
research_node: "Find vendors"
    β”‚ Tool: mock_vendor_search
    β–Ό Returns: vendor_options[]
analysis_node: "Evaluate & validate budget"
    β”œβ”€ LLM Call: Recommend vendor
    β”œβ”€ Tool: validate_budget
    β”‚ Returns: within_budget decision
    β”‚
    β”œβ”€ If within budget:
    β”‚  β–Ό
    β”‚ approval_gate: "Request human approval"
    β”‚  β”‚ State Checkpoint Created
    β”‚  β”‚ *** GRAPH PAUSES ***
    β”‚  β”‚ (External: Human updates state)
    β”‚  β–Ό
    β”‚ approval_decision: "Route based on approval"
    β”‚  β”‚
    β”‚  β”œβ”€ If approved:
    β”‚  β”‚  β–Ό
    β”‚  β”‚ legal_node: "Generate contract"
    β”‚  β”‚  β”‚ LLM Call: Create purchase agreement
    β”‚  β”‚  β–Ό
    β”‚  β”‚ contract_draft β†’ END
    β”‚  β”‚
    β”‚  └─ If rejected:
    β”‚     β–Ό
    β”‚     END
    β”‚
    └─ If exceeds budget:
       β–Ό
       END (Rejection)

Component Architecture

1. State Schema (TypedDict)

class ProcurementState(TypedDict):
    """Complete graph state with type safety."""
    
    # Input Fields
    procurement_request: str              # What: Raw requirement
    budget_limit: float                   # Budget: Maximum spend
    
    # Agent Output Fields
    vendor_options: list[dict]            # Research output: candidates
    selected_vendor: dict                 # Analysis output: chosen vendor
    
    # Decision Fields
    analysis_approved: bool               # Analysis: Budget check result
    human_approved: bool                  # Approval gate: Human decision
    
    # Final Output
    contract_draft: str                   # Legal output: Purchase agreement
    
    # Audit
    logs: list[str]                       # Complete decision history

Design Rationale:

  • Strict Types: TypedDict enables IDE autocomplete and type checking
  • Annotated[list, add]: Custom operator for list accumulation in logs
  • Immutable Pattern: State never modified directly; new values via Command updates
  • Audit Trail: Every decision logged with timestamp

2. Agents

ResearchAgent

Responsibility: Vendor Discovery

Input: procurement_request: str
Process: 
  1. Parse requirement
  2. Call vendor search tool
  3. Return structured vendor list
Output: vendor_options: list[dict]
Error Handling: Return empty list if search fails

Tool Integration:

@tool
def mock_vendor_search(query: str) -> list[dict]:
    """Mock vendor database search."""
    # Returns: [{"vendor_id", "name", "price_per_month", "capabilities", ...}]
    
# Production: Replace with real APIs
def real_vendor_search(query: str) -> list[dict]:
    return requests.get("https://api.vendors.com/search").json()

AnalysisAgent

Responsibility: Vendor Evaluation & Budget Validation

Input: 
  - vendor_options: list[dict]
  - budget_limit: float
  - procurement_request: str
  
Process:
  1. Use LLM to analyze vendors
     β”œβ”€ Prompt: "Recommend best vendor considering price, reputation, capabilities"
     β”œβ”€ Temperature: 0.3 (low randomness for consistency)
     └─ Max tokens: 2048
  2. Parse LLM response for recommendation
  3. Extract selected vendor from options
  4. Call budget_validator tool
     └─ Returns: {within_budget: bool, variance: float, status: "APPROVED|REJECTED"}
  5. Route based on budget result
     β”œβ”€ Within budget β†’ approval_gate
     └─ Exceeded β†’ END

Output: 
  - selected_vendor: dict
  - analysis_approved: bool
  
Routing: 
  - goto="approval_gate" (if approved)
  - goto=END (if rejected)

LLM Considerations:

  • Model: claude-3-5-sonnet-20241022 (balanced performance/cost)
  • Temperature: 0.3 (consistent financial analysis)
  • System Prompt: Embedded in PromptTemplate (financial context)
  • Token Limit: 2048 (sufficient for vendor analysis + response)

LegalAgent

Responsibility: Contract Generation

Input:
  - selected_vendor: dict
  - procurement_request: str
  - budget_limit: float
  
Process:
  1. Format vendor details for legal document
  2. Call LLM with contract template
     β”œβ”€ Prompt: "Generate professional purchase contract in markdown"
     β”œβ”€ Includes: Parties, services, pricing, terms, warranties
     └─ Output: Markdown document
  3. Format and return contract

Output:
  - contract_draft: str (markdown)
  
Routing:
  - goto=END (always terminates)

Contract Template Includes:

  • Header and document information
  • Parties (Company & Vendor)
  • Services/Goods description
  • Pricing and payment terms
  • Term and termination clauses
  • Liabilities and warranties
  • Confidentiality
  • Signature block

3. Tools

Vendor Search Tool

@tool
def mock_vendor_search(query: str) -> list[dict]:
    """
    Discover vendors matching procurement request.
    
    Mock Database Structure:
    - Categories: cloud_infrastructure, software_licensing
    - Per vendor: id, name, price, capabilities, reputation, terms
    
    Production Integration:
    - Query vendor API
    - Filter by region, compliance, integration support
    - Return ranked by relevance/price
    """

Budget Validator Tool

@tool
def validate_budget(vendor_price: float, budget_limit: float) -> dict:
    """
    Validate vendor pricing against budget.
    
    Returns:
    {
        "within_budget": True/False,
        "vendor_price": float,
        "budget_limit": float,
        "variance": float,               # budget - price
        "variance_percentage": float,
        "status": "APPROVED" | "REJECTED"
    }
    
    Production Integration:
    - Query ERP system for available budget
    - Check cost center allocation
    - Validate against PO limits
    - Return approval status
    """

4. Graph Structure

Nodes

START
  ↓
research_node (ResearchAgent)
  ↓ Command.goto="analysis_node"
analysis_node (AnalysisAgent)
  β”œβ”€ Command.goto="approval_gate" (within budget)
  └─ Command.goto=END (exceeds budget)
    ↓
approval_gate (Interruption)
  ↓ Command.goto="approval_decision"
approval_decision (Router)
  β”œβ”€ Route to "legal_node" (if human_approved)
  └─ Route to END (if not approved)
    ↓
legal_node (LegalAgent)
  ↓ Command.goto=END
END

Edges

# Linear flow
graph.add_edge(START, "research_node")

# Command-based routing (agents return Command with explicit goto)
# research_node β†’ analysis_node (via Command)
# analysis_node β†’ {approval_gate OR END} (via Command)

# Explicit edge for approval flow
graph.add_edge("approval_gate", "approval_decision")

# Conditional routing
graph.add_conditional_edges(
    "approval_decision",
    lambda state: "legal_node" if state.get("human_approved") else END
)

# Terminal edge
graph.add_edge("legal_node", END)

Checkpointing Configuration

checkpointer = MemorySaver()  # Development
# or
checkpointer = PostgresSaver()  # Production

compiled_graph = graph.compile(
    checkpointer=checkpointer,
    interrupt_before=["legal_node"]
)

# Interruption Semantics:
# - Graph executes all nodes until reaching legal_node
# - Before executing legal_node, graph PAUSES
# - Control returns to application
# - State persisted to checkpointer
# - External system updates state.human_approved
# - graph.update_state(config, {"human_approved": True})
# - graph.stream(None, config) resumes execution
# - Continues with approval_decision and legal_node

Execution Model

Single Execution Cycle

Timeline: T=0ms to T=60ms

T+0ms   Input: procurement_request, budget_limit
        Initial State created
        
T+5ms   research_node executes
        β”‚ Input: procurement_request
        β”œβ”€ Call: mock_vendor_search(request)
        β”‚ LangSmith: Log tool invocation
        └─ Output: vendor_options populated, goto="analysis_node"
        
T+10ms  analysis_node executes
        β”‚ Input: vendor_options, budget_limit, request
        β”œβ”€ Call: LLM with vendor analysis prompt
        β”‚ LangSmith: Log LLM call (tokens, latency, cost)
        β”œβ”€ Parse: Extract recommendation
        β”œβ”€ Call: validate_budget(vendor_price, budget)
        β”‚ LangSmith: Log tool invocation
        └─ Decide: 
           within_budget β†’ goto="approval_gate"
           exceeded β†’ goto=END
        
T+15ms  (if within budget) approval_gate executes
        β”‚ Input: selected_vendor, logs
        β”œβ”€ Create log entry
        └─ Return Command(goto="approval_decision")
        
T+16ms  *** GRAPH PAUSES ***
        State checkpointed to MemorySaver
        Control returns to application
        
        External: Human reviews state
        External: Calls executor.approve_vendor(approval=True)
        External: graph.update_state(config, {"human_approved": True})
        
T+60s+  External: graph.stream(None, config) called to resume
        
T+61ms  approval_decision executes
        β”‚ Input: human_approved=True (updated state)
        └─ Route: goto="legal_node"
        
T+70ms  legal_node executes
        β”‚ Input: selected_vendor, request
        β”œβ”€ Call: LLM with contract generation prompt
        β”‚ LangSmith: Log LLM call
        β”œβ”€ Format: Markdown contract
        └─ Output: contract_draft populated, goto=END
        
T+75ms  Graph reaches END
        Execution complete
        Final state returned to application

State Checkpoint Mechanics

Checkpoint Creation (T+16ms):

# MemorySaver internal representation
checkpointer.storage = {
    "procurement_20250628_145230": {
        "values": {
            # Full state at interruption point
            "procurement_request": "...",
            "vendor_options": [...],
            "selected_vendor": {...},
            "budget_limit": 5500.0,
            "analysis_approved": True,
            "human_approved": False,  # Checkpoint created here
            "contract_draft": "",
            "logs": [...]
        },
        "next": "approval_decision",  # Next node to execute
        "task_id": "...",
        "channel": "input"
    }
}

State Retrieval & Update:

# Retrieve saved state
config = {"configurable": {"thread_id": "procurement_20250628_145230"}}
saved_state = graph.get_state(config)  # Full state recovered

# Update with human decision
graph.update_state(config, {"human_approved": True})

# State in checkpoint is now:
# {..., "human_approved": True, ...}

# Resume from checkpoint
graph.stream(None, config)  # Continues from next=approval_decision

Observability & Monitoring

LangSmith Integration

Automatic Tracing:

Every LLM call, tool invocation, and state transition is logged to LangSmith.

Trace Structure:
β”Œβ”€ Procurement Workflow Run
β”‚  β”œβ”€ research_node
β”‚  β”‚  └─ Tool: mock_vendor_search
β”‚  β”‚     β”œβ”€ Input: {"query": "cloud infrastructure"}
β”‚  β”‚     β”œβ”€ Output: [3 vendors]
β”‚  β”‚     └─ Latency: 2ms
β”‚  β”‚
β”‚  β”œβ”€ analysis_node
β”‚  β”‚  β”œβ”€ LLM: ChatAnthropic
β”‚  β”‚  β”‚  β”œβ”€ Prompt: "Analyze vendors..."
β”‚  β”‚  β”‚  β”œβ”€ Response: "Based on analysis..."
β”‚  β”‚  β”‚  β”œβ”€ Tokens: 342 in, 198 out
β”‚  β”‚  β”‚  β”œβ”€ Cost: $0.0032
β”‚  β”‚  β”‚  └─ Latency: 1.23s
β”‚  β”‚  β”‚
β”‚  β”‚  └─ Tool: validate_budget
β”‚  β”‚     β”œβ”€ Input: {"vendor_price": 5000, "budget": 5500}
β”‚  β”‚     β”œβ”€ Output: {"within_budget": true, ...}
β”‚  β”‚     └─ Latency: 1ms
β”‚  β”‚
β”‚  β”œβ”€ approval_gate
β”‚  β”‚  └─ Status: INTERRUPTED (awaiting human approval)
β”‚  β”‚
β”‚  └─ legal_node
β”‚     └─ LLM: ChatAnthropic
β”‚        β”œβ”€ Prompt: "Generate contract..."
β”‚        β”œβ”€ Response: "# PURCHASE AGREEMENT..."
β”‚        β”œβ”€ Tokens: 287 in, 856 out
β”‚        β”œβ”€ Cost: $0.0056
β”‚        └─ Latency: 1.89s

Analytics Available:

  • Traces: View every run with full execution tree
  • Metrics: Token usage, latency by agent, cost per run
  • Datasets: Log examples for continuous improvement
  • Feedback: Mark runs as good/bad for analysis
  • Comparison: Compare LLM models or prompts

Audit Trail

State logs maintain complete decision history:

state["logs"] = [
    "Workflow initiated",
    "[2025-06-28T14:52:30] ResearchNode: Analyzing request: We need enterprise cloud... | Found 3 vendors.",
    "[2025-06-28T14:52:31] AnalysisNode: Validating 3 vendors against budget $5500.00 | Selected: CloudTech Solutions | Status: APPROVED",
    "[2025-06-28T14:52:31] ApprovalGate: Awaiting human approval for CloudTech Solutions",
    "[2025-06-28T14:52:35] LegalNode: Drafting contract for CloudTech Solutions | Contract generated successfully"
]

Compliance Features:

  • Timestamped decision points
  • Vendor selection rationale
  • Budget validation evidence
  • Human approval tracking
  • Contract generation audit

Failure Modes & Recovery

Failure Mode: Research Tool Fails

research_node β†’ Exception in mock_vendor_search
↓
Error: "No vendors found for query"
↓
Handling:
- Log error: {"error": "vendor_search failed", "request": "..."}
- Return empty vendor_options
- Next node (analysis_node) handles gracefully or routes to END
- User notified with audit trail

Failure Mode: LLM Call Fails

analysis_node β†’ Timeout in ChatAnthropic.invoke()
↓
Error: "Model request timed out"
↓
Handling:
- LangChain builtin: Automatic retry with exponential backoff
- Max retries: 3
- If all retry: Exception bubbles up
- Can be caught in node: return Command(goto=END, update={"error": "..."})

Failure Mode: Graph Paused, Process Crashes

T+16ms: Graph paused, state checkpointed
T+30s:  Process crashes
T+50s:  New process starts with same thread_id
↓
Recovery:
1. Create new graph instance
2. Call: saved_state = graph.get_state(config)
   β†’ Retrieves full state from checkpoint
3. Update: graph.update_state(config, {"human_approved": True})
   β†’ Modifies checkpointed state
4. Resume: graph.stream(None, config)
   β†’ Continues from approval_decision node
5. Final state: Returned with contract_draft populated

Zero-Loss Recovery: No lost state due to checkpointing.

Failure Mode: Budget Exceeds

analysis_node β†’ validate_budget(6000, 5000) β†’ {"within_budget": False}
↓
Handling:
- Set analysis_approved = False
- Route to END (not approval_gate)
- Contract never generated
- Audit log shows rejection reason
- User notified: "Budget exceeded by $1000"

Scalability Considerations

Current Architecture (MemorySaver)

  • Single Process: State held in memory
  • Single Machine: No inter-process state sharing
  • Suitable For: POC, development, low-volume deployments

Production Architecture (PostgresSaver)

Recommended Setup:

Multiple Worker Processes ↔ PostgreSQL Database
                             (Shared State)
                             ↓ Checkpointing
                             ↓ Shared thread storage

Benefits:

  • Horizontal Scaling: Add workers for concurrent workflows
  • Persistence: State survives process restarts
  • Distributed: Workers on different machines
  • Concurrent Workflows: 1000s of independent procurement requests

Estimated Capacity:

  • 1 PostgreSQL instance: 10,000+ concurrent workflow states
  • 1 worker process: 10-50 concurrent workflows (I/O limited)
  • Scale out: Add more workers as needed

Performance Optimization

Latency Breakdown (Current):

research_node: ~5ms (mock tool, instant)
analysis_node: ~1,200ms (1 LLM call)
  β”œβ”€ LLM latency: ~1,200ms
  └─ Tool latency: ~1ms
legal_node: ~1,900ms (1 LLM call)
  β”œβ”€ LLM latency: ~1,900ms (longer response)
  └─ Tool latency: ~0ms

Total: ~3.1 seconds execution
Human approval: 60+ seconds (external)

Optimization Opportunities:

  1. Parallel Tool Calls: Use asyncio for mock_vendor_search + validate_budget
  2. Model Selection: Use claude-3-haiku for analysis (cheaper, faster)
  3. Prompt Optimization: Reduce tokens in analysis prompt
  4. Caching: Cache vendor list if query repeats
  5. Batch Processing: Run multiple workflows with thread pools

Cost Optimization

Current Pricing (Claude 3.5 Sonnet):

  • Input tokens: $0.003 per 1K
  • Output tokens: $0.015 per 1K

Per-Workflow Cost:

  • Analysis node: ~$0.0032 (342 in + 198 out)
  • Legal node: ~0.0056 (287 in + 856 out)
  • Total: ~$0.0088 per complete workflow

Cost Reduction:

  • Switch to Haiku: ~90% cost reduction
  • Optimize prompts: Reduce token usage
  • Cache responses: Avoid repeated calls
  • Batch similar requests: Amortize overhead

Security Architecture

Data Protection

  • State Checkpoint: Stored in checkpointer (MemorySaver or PostgreSQL)
  • Production: Encrypt at rest (PostgreSQL TLS)
  • Secrets: Never logged or traced
  • Audit: All decisions logged with timestamps

Access Control

  • Graph Access: Gated by thread_id (implicit isolation)
  • State Updates: Only via authenticated graph.update_state() calls
  • Token Storage: Never persisted (LLM calls ephemeral)
  • API Keys: Environment variables, not in code

Compliance

  • Audit Trail: Complete decision history in state
  • Timestamping: All actions timestamped
  • Approval Tracking: human_approved flag with external update tracking
  • Data Retention: Checkpoint retention policy configurable

Extension Points

Adding New Agents

def new_agent_node(state: ProcurementState) -> Command:
    """New specialized agent."""
    # Process state
    new_state = {"field": value}
    return Command(update=new_state, goto="next_node")

# Add to graph
graph.add_node("new_agent", new_agent_node)
graph.add_edge("previous_node", "new_agent")

Adding New Tools

@tool
def new_tool(param: str) -> dict:
    """Specialized tool for domain."""
    # Implement business logic
    return {"result": "..."}

# Use in agent nodes
result = self.new_tool(state["field"])

Custom Routing

def custom_router(state: ProcurementState) -> str:
    """Complex routing logic."""
    if state["field"] == "value":
        return "node_a"
    else:
        return "node_b"

graph.add_conditional_edges("source", custom_router)

Integration with External Systems

# Send approval request
await send_slack_notification(state)

# Sync to document management
await upload_contract_to_docusign(state["contract_draft"])

# Log to compliance system
await log_to_audit_trail(state["logs"])

Conclusion

This architecture provides a production-grade foundation for multi-agent procurement systems. Key strengths:

  1. Modularity: Independent agents, tools, and routing
  2. Observability: Full tracing via LangSmith
  3. Reliability: State persistence and recovery
  4. Extensibility: Easy to add agents, tools, and routing
  5. Compliance: Complete audit trail and approval tracking

The system is suitable for immediate POC deployment and scales to production with PostgreSQL checkpointing and real tool integrations.