The hybrid subagent

Claims questions sit in an awkward middle ground. A user asking 'is water damage covered?' needs the policy KB. A user asking 'what is the status of CLM-5003?' needs the database. A user asking 'I want to file a claim for $5600 for water damage' needs both the policy (to check coverage language) and the database (to actually create the claim). The claims subagent handles all three shapes.

agent_graph.py
python
CLAIMS_PROMPT = '''You are the claims agent. Use BOTH the policy excerpts and the
customer's claim history to answer. You can acknowledge a new claim intent but do
not promise approval; new claims enter the system in 'review' status.

User question:
{user_input}

Customer claims (JSON):
{claims}

Relevant policy excerpts:
{context}

Answer professionally. If this is a new claim filing, summarize what was opened.
'''

The prompt is explicit about two grounding sources AND about a guardrail: the subagent cannot promise approval. New claims always enter in 'review' status. That rule is a product decision, not a model decision, and it belongs in the prompt and in the code.

agent_graph.py
python
async def claims_agent_node(state: ClaimState) -> Dict[str, Any]:
    customer_id = state.get('customer_id')
    user_input = state.get('user_input', '')
    claims = list_claims(customer_id) if customer_id else []

    filing_intent = any(
        phrase in user_input.lower()
        for phrase in ('file a claim', 'open a claim', 'new claim', 'submit a claim')
    )
    if filing_intent and customer_id:
        amount_match = re.search(r'\$?([0-9]+(?:\.[0-9]+)?)', user_input)
        amount = float(amount_match.group(1)) if amount_match else 0.0
        opened = open_claim(customer_id, claim_type='general', amount=amount)
        claims = [opened] + claims

    hits = search_policies(user_input, k=3)
    context = '\n\n'.join(f"[source: {h['source']}] {h['content']}" for h in hits) or '(no policy matches)'

    provider = get_llm_provider()
    answer = await provider.generate_text(
        CLAIMS_PROMPT.format(
            user_input=user_input,
            claims=json.dumps(claims, default=str),
            context=context,
        ),
        max_tokens=500,
        temperature=0.2,
    )
    return {
        'claims_record': claims,
        'retrieved_docs': hits,
        'answer': answer,
        'citations': sorted({h['source'] for h in hits}),
    }

A cheap keyword-based filing intent check decides whether to actually create a claim. When intent plus customer_id are both present, open_claim runs and the new claim is prepended to the list passed into the prompt. Both RAG and SQL results end up in state so a follow-up turn can reuse them.

Because intent detection here triggers a side effect: writing a new row to the database. You want that trigger to be deterministic and easy to reason about. A keyword gate catches obvious filing intent ("file a claim", "open a claim"). An LLM classifier would add latency, cost, and nondeterminism. If the keyword gate misses an edge case, the worst that happens is the user gets a conversational answer and tries again. If an LLM classifier hallucinates intent, you get a spurious claim row.

Flashcards: Flashcards

Loading practice…