Coverage for astrocyte/mip/intent.py: 76%
42 statements
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-04 05:24 +0000
« prev ^ index » next coverage.py v7.15.0, created at 2026-07-04 05:24 +0000
1"""MIP intent layer — LLM-based routing when mechanical rules can't resolve.
3Async — requires LLM call. Follows the pattern of pipeline/curated_retain.py.
4"""
6from __future__ import annotations
8import json
9import logging
10from typing import TYPE_CHECKING
12from astrocyte.mip.schema import BankDefinition, IntentPolicy
13from astrocyte.types import Message, RoutingDecision
15if TYPE_CHECKING:
16 from astrocyte.mip.rule_engine import RuleEngineInput
17 from astrocyte.provider import LLMProvider
19logger = logging.getLogger("astrocyte.mip")
22async def resolve_intent(
23 input_data: RuleEngineInput,
24 intent_policy: IntentPolicy,
25 available_banks: list[BankDefinition],
26 llm_provider: LLMProvider,
27 *,
28 model: str | None = None,
29) -> RoutingDecision:
30 """Ask the LLM to route content when mechanical rules cannot resolve.
32 System message contains routing instructions (trusted).
33 User message wraps untrusted content in XML delimiters.
34 Falls back to passthrough on failure.
35 """
36 max_tokens = 200
37 if intent_policy.constraints and "max_tokens" in intent_policy.constraints:
38 max_tokens = int(intent_policy.constraints["max_tokens"])
40 system_msg = _build_system_message(intent_policy, available_banks)
41 user_msg = _build_user_message(input_data)
43 try:
44 completion = await llm_provider.complete(
45 messages=[
46 Message(role="system", content=system_msg),
47 Message(role="user", content=user_msg),
48 ],
49 max_tokens=max_tokens,
50 temperature=0,
51 )
52 return _parse_intent_response(completion.text)
53 except Exception:
54 logger.warning("MIP intent layer failed, falling back to passthrough")
55 return RoutingDecision(resolved_by="passthrough", reasoning="Intent layer LLM call failed")
58def _build_system_message(
59 intent_policy: IntentPolicy,
60 available_banks: list[BankDefinition],
61) -> str:
62 """Build the system message with routing instructions (trusted content only)."""
63 banks_str = ", ".join(b.id for b in available_banks) if available_banks else "(none defined)"
65 if intent_policy.model_context:
66 base = intent_policy.model_context.replace("{banks}", banks_str)
67 else:
68 base = (
69 f"You are a memory routing agent. Route content to the correct bank and apply tags.\n"
70 f"Available banks: {banks_str}\n"
71 f"Never override compliance rules."
72 )
74 return f"""{base}
76The user message contains the content to route inside <content> XML tags, along with metadata.
77Respond with a JSON object:
78{{"bank_id": "...", "tags": ["..."], "retain_policy": "default", "reasoning": "..."}}"""
81def _build_user_message(input_data: RuleEngineInput) -> str:
82 """Build the user message with untrusted content wrapped in XML delimiters."""
83 tags_str = ", ".join(input_data.tags) if input_data.tags else "(none)"
84 return f"""<content>
85{input_data.content[:500]}
86</content>
88Content type: {input_data.content_type or "text"}
89Source: {input_data.source or "unknown"}
90Tags: {tags_str}
91PII detected: {input_data.pii_detected}"""
94def _parse_intent_response(response: str) -> RoutingDecision:
95 """Parse LLM JSON response into RoutingDecision. Graceful fallback."""
96 try:
97 text = response.strip()
98 # Extract from code block if present
99 if "```" in text:
100 start = text.index("```") + 3
101 if text[start:].startswith("json"):
102 start += 4
103 end = text.index("```", start)
104 text = text[start:end].strip()
106 data = json.loads(text)
107 return RoutingDecision(
108 bank_id=data.get("bank_id"),
109 tags=data.get("tags"),
110 retain_policy=data.get("retain_policy"),
111 resolved_by="intent",
112 confidence=data.get("confidence", 0.8),
113 reasoning=data.get("reasoning"),
114 )
115 except (json.JSONDecodeError, ValueError, KeyError):
116 logger.warning("Failed to parse MIP intent response, falling back to passthrough")
117 return RoutingDecision(resolved_by="passthrough", reasoning="Failed to parse intent response")