Download procurement_system.py from PrernaKu/LegalDoc: direct link, hf CLI and curl.
- Browser
- Download file 16.4 kB
-
https://huggingface.co/spaces/PrernaKu/LegalDoc/resolve/main/procurement_system.py
- Command line
-
hf download hf://spaces/PrernaKu/LegalDoc/procurement_system.py
-
curl -L -o procurement_system.py https://huggingface.co/spaces/PrernaKu/LegalDoc/resolve/main/procurement_system.py
16.4 kB
| """ | |
| Multi-Agent Autonomous Procurement System | |
| LangGraph + LangChain (Groq / Llama-3.3-70b) + LangSmith | |
| """ | |
| import os | |
| import re | |
| import json | |
| import uuid | |
| from typing import TypedDict, Annotated | |
| from datetime import datetime | |
| from operator import add | |
| # ββ LangSmith must be configured BEFORE any LangChain import ββββββββββββββββββ | |
| os.environ["LANGCHAIN_TRACING_V2"] = "true" | |
| os.environ["LANGCHAIN_API_KEY"] = "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" | |
| os.environ["LANGCHAIN_PROJECT"] = "multi_agent_procurement" | |
| os.environ["LANGCHAIN_ENDPOINT"] = "https://api.smith.langchain.com" | |
| from langgraph.graph import StateGraph, START, END | |
| from langgraph.checkpoint.memory import MemorySaver | |
| from langgraph.types import Command | |
| from langchain_groq import ChatGroq | |
| from langchain_core.language_models import BaseLanguageModel | |
| from langchain_core.messages import HumanMessage | |
| # ββ Credentials ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| GROQ_API_KEY = "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" | |
| GROQ_MODEL = "llama-3.3-70b-versatile" | |
| os.environ["GROQ_API_KEY"] = GROQ_API_KEY | |
| # ============================================================================= | |
| # 1. STATE | |
| # ============================================================================= | |
| class ProcurementState(TypedDict): | |
| raw_input: str | |
| procurement_request: str | |
| vendor_options: list | |
| selected_vendor: dict | |
| budget_limit: float | |
| analysis_approved: bool | |
| human_approved: bool | |
| contract_draft: str | |
| logs: Annotated[list, add] | |
| def _empty_state() -> dict: | |
| return { | |
| "raw_input": "", | |
| "procurement_request": "", | |
| "vendor_options": [], | |
| "selected_vendor": {}, | |
| "budget_limit": 5000.0, | |
| "analysis_approved": False, | |
| "human_approved": False, | |
| "contract_draft": "", | |
| "logs": [], | |
| } | |
| # ============================================================================= | |
| # 2. VENDOR DATABASE (plain functions β no @tool wrapper) | |
| # ============================================================================= | |
| def mock_vendor_search(query: str) -> list: | |
| db = { | |
| "cloud_infrastructure": [ | |
| { | |
| "vendor_id": "VENDOR_001", | |
| "name": "CloudTech Solutions", | |
| "service_type": "Cloud Infrastructure", | |
| "price_per_month": 5000, | |
| "capabilities": ["Auto-scaling", "99.99% Uptime SLA", "24/7 Support"], | |
| "reputation_score": 9.2, | |
| "contract_terms": "12-month minimum", | |
| }, | |
| { | |
| "vendor_id": "VENDOR_002", | |
| "name": "InfraSpeed Inc", | |
| "service_type": "Cloud Infrastructure", | |
| "price_per_month": 3500, | |
| "capabilities": ["Auto-scaling", "99.9% Uptime SLA", "Business hours support"], | |
| "reputation_score": 8.5, | |
| "contract_terms": "6-month minimum", | |
| }, | |
| { | |
| "vendor_id": "VENDOR_003", | |
| "name": "EcoCloud Global", | |
| "service_type": "Cloud Infrastructure", | |
| "price_per_month": 6200, | |
| "capabilities": ["Auto-scaling", "99.999% Uptime SLA", "24/7 Premium Support", "Green energy"], | |
| "reputation_score": 9.5, | |
| "contract_terms": "12-month minimum", | |
| }, | |
| ], | |
| "software_licensing": [ | |
| { | |
| "vendor_id": "VENDOR_004", | |
| "name": "ProSoft Licensing", | |
| "service_type": "Enterprise Software", | |
| "price_per_month": 2000, | |
| "capabilities": ["1000 user licenses", "Cloud deployment", "Annual updates"], | |
| "reputation_score": 8.8, | |
| "contract_terms": "Annual renewal", | |
| }, | |
| { | |
| "vendor_id": "VENDOR_005", | |
| "name": "NextGen Software Co", | |
| "service_type": "Enterprise Software", | |
| "price_per_month": 2800, | |
| "capabilities": ["2000 user licenses", "Cloud + On-prem", "Quarterly updates", "Custom integrations"], | |
| "reputation_score": 9.1, | |
| "contract_terms": "Annual renewal", | |
| }, | |
| ], | |
| } | |
| q = query.lower() | |
| category = "cloud_infrastructure" if any(w in q for w in ["cloud", "server", "infra", "hosting"]) else "software_licensing" | |
| return db[category] | |
| def validate_budget(vendor_price: float, budget_limit: float) -> dict: | |
| variance = budget_limit - vendor_price | |
| within = vendor_price <= budget_limit | |
| pct = round(variance / budget_limit * 100, 2) if budget_limit > 0 else 0 | |
| return { | |
| "within_budget": within, | |
| "vendor_price": vendor_price, | |
| "budget_limit": budget_limit, | |
| "variance": variance, | |
| "variance_percentage": pct, | |
| "status": "APPROVED" if within else "REJECTED", | |
| } | |
| # ============================================================================= | |
| # 3. LLM HELPER β plain text call, strips markdown fences, returns str | |
| # ============================================================================= | |
| def _llm_call(llm: ChatGroq, prompt: str) -> str: | |
| response = llm.invoke([HumanMessage(content=prompt)]) | |
| text = response.content.strip() | |
| # Strip ```json ... ``` or ``` ... ``` | |
| text = re.sub(r"^```[a-zA-Z]*\n?", "", text) | |
| text = re.sub(r"\n?```$", "", text) | |
| return text.strip() | |
| # ============================================================================= | |
| # 4. AGENT NODES | |
| # ============================================================================= | |
| class ProcurementAgents: | |
| def __init__(self, llm: ChatGroq): | |
| self.llm = llm | |
| # ββ Agent 0: Prose β Structured ββββββββββββββββββββββββββββββββββββββββββ | |
| def parse_node(self, state: ProcurementState) -> Command: | |
| raw = (state.get("raw_input") or "").strip() | |
| req = (state.get("procurement_request") or "").strip() | |
| ts = datetime.now().strftime("%H:%M:%S") | |
| # If no prose, just pass through whatever structured request was given | |
| if not raw: | |
| log = f"[{ts}] ParseNode: Structured input β skipping prose extraction." | |
| return Command( | |
| update={"logs": [log]}, | |
| goto="research_node", | |
| ) | |
| log = f"[{ts}] ParseNode: Extracting structured fields from prose..." | |
| prompt = ( | |
| "You are a procurement intake assistant. Extract structured procurement details " | |
| "from the user description below.\n\n" | |
| f"User input: {raw}\n\n" | |
| "Reply with ONLY valid JSON, no markdown:\n" | |
| '{\n' | |
| ' "procurement_request": "<1-2 sentence professional description>",\n' | |
| ' "budget_limit": <number, monthly budget, default 5000 if not mentioned>,\n' | |
| ' "confidence": "<high|medium|low>"\n' | |
| '}' | |
| ) | |
| try: | |
| content = _llm_call(self.llm, prompt) | |
| parsed = json.loads(content) | |
| req = parsed.get("procurement_request") or raw | |
| budget = float(parsed.get("budget_limit") or state.get("budget_limit") or 5000) | |
| conf = parsed.get("confidence", "medium") | |
| log += f" | Request: '{req[:60]}' | Budget: ${budget:,.0f} | Confidence: {conf}" | |
| except Exception as e: | |
| # Fallback: raw text, keep existing budget | |
| req = raw | |
| budget = float(state.get("budget_limit") or 5000) | |
| log += f" | Parse failed ({e}), using raw input verbatim." | |
| return Command( | |
| update={"procurement_request": req, "budget_limit": budget, "logs": [log]}, | |
| goto="research_node", | |
| ) | |
| # ββ Agent 1: Research βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def research_node(self, state: ProcurementState) -> Command: | |
| request = (state.get("procurement_request") or "").strip() | |
| ts = datetime.now().strftime("%H:%M:%S") | |
| if not request: | |
| log = f"[{ts}] ResearchNode: No request text β aborting." | |
| return Command(update={"logs": [log]}, goto=END) | |
| vendors = mock_vendor_search(request) | |
| log = f"[{ts}] ResearchNode: Found {len(vendors)} vendor(s) for '{request[:50]}...'" | |
| return Command( | |
| update={"vendor_options": vendors, "logs": [log]}, | |
| goto="analysis_node", | |
| ) | |
| # ββ Agent 2: Financial Analysis βββββββββββββββββββββββββββββββββββββββββββ | |
| def analysis_node(self, state: ProcurementState) -> Command: | |
| vendors = state.get("vendor_options") or [] | |
| budget = float(state.get("budget_limit") or 5000) | |
| ts = datetime.now().strftime("%H:%M:%S") | |
| log = f"[{ts}] AnalysisNode: Evaluating {len(vendors)} vendor(s) vs budget ${budget:,.0f}" | |
| if not vendors: | |
| log += " | No vendors to evaluate." | |
| return Command(update={"analysis_approved": False, "logs": [log]}, goto=END) | |
| prompt = ( | |
| f"You are a financial analyst. Budget limit: ${budget}/month.\n" | |
| f"Vendors:\n{json.dumps(vendors, indent=2)}\n\n" | |
| "Pick the BEST vendor whose price_per_month is AT OR BELOW the budget. " | |
| "If none fit, pick the cheapest one.\n" | |
| "Reply with ONLY valid JSON:\n" | |
| '{"recommended_vendor_id": "<vendor_id>", "rationale": "<one sentence>"}' | |
| ) | |
| try: | |
| content = _llm_call(self.llm, prompt) | |
| parsed = json.loads(content) | |
| rec_id = parsed.get("recommended_vendor_id", "") | |
| # Find the recommended vendor; fallback to cheapest | |
| selected = min(vendors, key=lambda v: v["price_per_month"]) | |
| for v in vendors: | |
| if v["vendor_id"] == rec_id: | |
| selected = v | |
| break | |
| except Exception: | |
| selected = min(vendors, key=lambda v: v["price_per_month"]) | |
| result = validate_budget(selected["price_per_month"], budget) | |
| log += f" | Selected: {selected['name']} (${selected['price_per_month']:,}/mo) | {result['status']}" | |
| if result["within_budget"]: | |
| return Command( | |
| update={"selected_vendor": selected, "analysis_approved": True, "logs": [log]}, | |
| goto="approval_gate", | |
| ) | |
| else: | |
| log += " | All vendors exceed budget β workflow rejected." | |
| return Command( | |
| update={"selected_vendor": selected, "analysis_approved": False, "logs": [log]}, | |
| goto=END, | |
| ) | |
| # ββ Agent 3: Legal ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def legal_node(self, state: ProcurementState) -> Command: | |
| vendor = state.get("selected_vendor") or {} | |
| request = state.get("procurement_request") or "" | |
| ts = datetime.now().strftime("%H:%M:%S") | |
| log = f"[{ts}] LegalNode: Drafting contract for {vendor.get('name', 'Unknown')}" | |
| prompt = ( | |
| "Draft a professional PURCHASE AGREEMENT in plain text (no markdown headers).\n\n" | |
| f"Vendor: {vendor.get('name', 'N/A')}\n" | |
| f"Service: {vendor.get('service_type', 'N/A')}\n" | |
| f"Monthly Cost: ${vendor.get('price_per_month', 0):,}\n" | |
| f"Capabilities: {', '.join(vendor.get('capabilities', []))}\n" | |
| f"Contract Terms: {vendor.get('contract_terms', 'Standard')}\n" | |
| f"Procurement Need: {request}\n\n" | |
| "Include: Parties, Services, Pricing & Payment, Term & Termination, " | |
| "Warranties, Confidentiality, Signatures." | |
| ) | |
| contract = _llm_call(self.llm, prompt) | |
| log += " | Contract generated successfully." | |
| return Command( | |
| update={"contract_draft": contract, "logs": [log]}, | |
| goto=END, | |
| ) | |
| # ============================================================================= | |
| # 5. APPROVAL GATE & ROUTER | |
| # ============================================================================= | |
| def approval_gate(state: ProcurementState) -> dict: | |
| vendor_name = (state.get("selected_vendor") or {}).get("name", "Unknown") | |
| ts = datetime.now().strftime("%H:%M:%S") | |
| log = f"[{ts}] ApprovalGate: Paused β awaiting human decision for '{vendor_name}'" | |
| return {"logs": [log]} | |
| def _approval_router(state: ProcurementState) -> str: | |
| approved = state.get("human_approved") | |
| return "legal_node" if approved else END | |
| # ============================================================================= | |
| # 6. GRAPH | |
| # ============================================================================= | |
| def build_procurement_graph(llm: ChatGroq) -> tuple: | |
| agents = ProcurementAgents(llm) | |
| graph = StateGraph(ProcurementState) | |
| graph.add_node("parse_node", agents.parse_node) | |
| graph.add_node("research_node", agents.research_node) | |
| graph.add_node("analysis_node", agents.analysis_node) | |
| graph.add_node("approval_gate", approval_gate) | |
| graph.add_node("legal_node", agents.legal_node) | |
| graph.add_edge(START, "parse_node") | |
| # parse_node / research_node / analysis_node route via Command.goto | |
| graph.add_conditional_edges("approval_gate", _approval_router) | |
| graph.add_edge("legal_node", END) | |
| memory = MemorySaver() | |
| compiled = graph.compile(checkpointer=memory, interrupt_before=["legal_node"]) | |
| return compiled, memory, agents | |
| # ============================================================================= | |
| # 7. EXECUTOR | |
| # ============================================================================= | |
| class ProcurementWorkflowExecutor: | |
| def __init__(self, graph, checkpointer): | |
| self.graph = graph | |
| self.checkpointer = checkpointer | |
| # UUID ensures no thread_id collisions across rapid calls | |
| self.thread_id = f"proc_{uuid.uuid4().hex[:12]}" | |
| def _config(self) -> dict: | |
| return {"configurable": {"thread_id": self.thread_id}} | |
| def _safe_state(self) -> dict: | |
| snap = self.graph.get_state(self._config()) | |
| if snap is None or snap.values is None: | |
| return _empty_state() | |
| return dict(snap.values) | |
| def start_workflow(self, procurement_request: str, budget_limit: float, raw_input: str = "") -> tuple: | |
| initial = { | |
| **_empty_state(), | |
| "raw_input": raw_input.strip(), | |
| "procurement_request": procurement_request.strip(), | |
| "budget_limit": float(budget_limit), | |
| "logs": [f"Workflow started at {datetime.now().strftime('%H:%M:%S')}"], | |
| } | |
| events = [] | |
| for event in self.graph.stream(initial, self._config(), stream_mode="updates"): | |
| events.append(event) | |
| return self._safe_state(), events | |
| def get_state(self) -> dict: | |
| return self._safe_state() | |
| def approve_vendor(self, approval: bool) -> tuple: | |
| self.graph.update_state(self._config(), {"human_approved": approval}) | |
| events = [] | |
| for event in self.graph.stream(None, self._config(), stream_mode="updates"): | |
| events.append(event) | |
| return self._safe_state(), events | |
| # ============================================================================= | |
| # 8. LLM FACTORY | |
| # ============================================================================= | |
| def create_llm() -> ChatGroq: | |
| return ChatGroq( | |
| model=GROQ_MODEL, | |
| api_key=GROQ_API_KEY, | |
| temperature=0.3, | |
| max_tokens=4096, | |
| ) | |