qualifier / backend /app /qualifier_models.py
Sethu0330's picture
Add FTL RFQ Qualifier agent + full SALES CRM app for HF Spaces testing
005df22 unverified
Raw History Blame Contribute Delete
7.03 kB
"""
Pydantic models for the RFQ Qualifier agent — skill CRUD (mirrors ChatAgentSkill* in models.py
one-for-one, but kept as its own independent set per the "separate skill system per agent"
decision) plus the qualification-run request/response and the structured decision schema the
agent's final tool call must satisfy.
"""
from pydantic import BaseModel, Field
from typing import Optional, List, Literal
# ---------------------------------------------------------------------------
# Skill CRUD — same anatomy as ChatAgentSkill* in models.py (name/description/instructions,
# references/assets/templates/scripts/evals), independent request models per the "separate copy
# per agent" decision.
# ---------------------------------------------------------------------------
class QualifierSkillReferenceItem(BaseModel):
title: str = Field("", max_length=300)
content: str = Field("", max_length=20000)
class QualifierSkillAssetItem(BaseModel):
name: str = Field("", max_length=300)
content: str = Field("", max_length=20000)
class QualifierSkillTemplateItem(BaseModel):
name: str = Field("", max_length=300)
content: str = Field("", max_length=20000)
class QualifierSkillScriptItem(BaseModel):
# The live background agent never executes this content. An admin can run it explicitly via
# the sandboxed /scripts/{index}/run route (subprocess-isolated, secrets stripped, timed out) —
# see qualifier_script_runner.py.
name: str = Field("", max_length=300)
description: str = Field("", max_length=2000)
language: str = Field("", max_length=40)
content: str = Field("", max_length=20000)
last_run_status: Optional[str] = None
last_run_output: Optional[str] = None
last_run_at: Optional[str] = None
class QualifierSkillEvalItem(BaseModel):
"""Unlike the chat agent's evals (a prompt + text assertion), a qualifier eval runs a real
stored sample RFQ (input_ref points at a QualifierRun or an uploaded fixture) through the full
pipeline and checks the resulting decision — closer to a regression test than a prompt probe."""
name: str = Field("", max_length=300)
input_ref: str = Field(
"", max_length=500, description="Reference to the fixture input, e.g. 'fixture:120-bloor-st-e.eml'"
)
expected_decision: Optional[Literal["qualify", "disqualify", "needs_review"]] = None
expected_project_type: Optional[Literal["modernization", "new_construction", "unknown"]] = None
assertion_type: Literal["decision_equals", "must_contain", "must_not_contain", "llm_judge"] = "decision_equals"
assertion_value: str = Field("", max_length=2000)
last_run_status: Optional[str] = None
last_run_detail: Optional[str] = None
last_run_at: Optional[str] = None
class QualifierSkillCreateRequest(BaseModel):
name: str = Field(..., min_length=1, max_length=200)
description: str = Field("", max_length=1000)
instructions: str = Field(..., min_length=1, max_length=14000)
references: List[QualifierSkillReferenceItem] = Field(default_factory=list, max_length=20)
assets: List[QualifierSkillAssetItem] = Field(default_factory=list, max_length=20)
templates: List[QualifierSkillTemplateItem] = Field(default_factory=list, max_length=20)
scripts: List[QualifierSkillScriptItem] = Field(default_factory=list, max_length=20)
evals: List[QualifierSkillEvalItem] = Field(default_factory=list, max_length=20)
class QualifierSkillPatchRequest(BaseModel):
name: Optional[str] = Field(None, min_length=1, max_length=200)
description: Optional[str] = Field(None, max_length=1000)
instructions: Optional[str] = Field(None, min_length=1, max_length=14000)
references: Optional[List[QualifierSkillReferenceItem]] = Field(None, max_length=20)
assets: Optional[List[QualifierSkillAssetItem]] = Field(None, max_length=20)
templates: Optional[List[QualifierSkillTemplateItem]] = Field(None, max_length=20)
scripts: Optional[List[QualifierSkillScriptItem]] = Field(None, max_length=20)
evals: Optional[List[QualifierSkillEvalItem]] = Field(None, max_length=20)
is_active: Optional[bool] = None
sort_order: Optional[int] = None
class QualifierSkillImportRequest(BaseModel):
content: str = Field(..., min_length=1, max_length=100000)
# ---------------------------------------------------------------------------
# Qualification run — the structured decision object is what the agent's final forced tool call
# ("submit_qualification_decision") must produce; OpenAI-style tool-calling validates the call
# arguments against this shape at generation time, so this schema doubles as the tool's JSON Schema
# (see qualifier_agent.py's DECISION_TOOL_SCHEMA, built from this model) and as the API response
# model / QualifierRun.result_json shape.
# ---------------------------------------------------------------------------
class QualifierMatchedItem(BaseModel):
item: str = Field(..., max_length=500, description="The RFQ line item as described in the spec")
category: str = Field(
...,
max_length=100,
description="door_operator | clutch | panel_adaptor | car_door_panel | roller_guide | governor | car_safety | door_protective_device | other_in_scope",
)
match: Literal["exact", "ambiguous"] = "exact"
catalog_ref: Optional[str] = Field(None, max_length=200, description="Matched pricelist catalog code, if exact")
note: str = Field("", max_length=1000)
class QualifierExcludedItem(BaseModel):
item: str = Field(..., max_length=500)
reason: str = Field(..., max_length=300, description="Short out-of-scope category, e.g. 'motor', 'controller', 'cab_interior', 'wiring'")
class QualifierDecision(BaseModel):
qualify: Literal["qualify", "disqualify", "needs_review"]
project_type: Literal["modernization", "new_construction", "unknown"]
matched_items: List[QualifierMatchedItem] = Field(default_factory=list)
excluded_items: List[QualifierExcludedItem] = Field(default_factory=list)
flags: List[str] = Field(default_factory=list, description="e.g. 'needs_engineering_review', 'missing_deadline', 'competitor_already_awarded'")
deadline: Optional[str] = Field(None, max_length=100, description="Stated bid/quote deadline, as found in the RFQ, or null if not stated")
project_name: str = Field("", max_length=300)
reasoning: str = Field(..., max_length=4000, description="Why this decision, referencing the qualify policy")
confidence: float = Field(..., ge=0.0, le=1.0)
class QualifierRunResponse(BaseModel):
id: int
status: Literal["running", "complete", "error"]
decision: Optional[str] = None
project_type: Optional[str] = None
confidence: Optional[float] = None
result: Optional[QualifierDecision] = None
error_detail: Optional[str] = None
project_name: str = ""
sender_email: str = ""
input_filename: str = ""
total_tokens: int = 0
started_at: Optional[str] = None
finished_at: Optional[str] = None