Add Attorney AI agent (substantive strategy memo)

- fl.attorney.agent (AbstractModel): manual-only substantive analysis fired from
  the case AI tab. Builds a full case context (parties, children, financials,
  issue tags, prior analyses) plus candidate statute/caselaw lists, and asks
  Claude to author a strategy memo, draft arguments/counterarguments, write a
  risk narrative, and assess substantial change (FL 61.30(1)(b))
- Grounds output in the real library: the model may only pick statutes/case law
  from the supplied candidates, which are then resolved back to records and
  linked (fl.analysis.cited_statute_ids / matched_caselaw_ids, case.caselaw_ids)
- Rule-based fallback produces a usable memo (complexity, statutes by category,
  caselaw by tag, risk flags) when the API is unavailable — never a raw error
- fl.analysis: add analysis_type, strategy_memo (Html), risk_narrative,
  cited_statute_ids; surface them in the analysis views
- fl.case: add attorney_memo_id + related memo/risk display; action_run_attorney_agent
  opens the memo; "Generate Attorney Strategy Memo" button on the AI tab (admin)
- Refactor fl_ai_engine: extract shared call_claude_json(system, user) +
  _extract_json so both agents and the engine share one Claude/JSON path

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-29 00:12:20 +00:00
parent 23c54b1b9f
commit 465c049251
7 changed files with 441 additions and 13 deletions

View File

@@ -391,9 +391,15 @@ Respond with the JSON object only. No other text."""
# ──────────────────────────────────────────────────────────────────────────
def _call_claude(self, prompt):
"""Call Claude with a single user prompt; return parsed JSON dict."""
return self.call_claude_json(prompt)
def call_claude_json(self, user_content, system=None, max_tokens=2048):
"""
Call Claude API and return parsed JSON dict.
Raises RuntimeError on connection error or JSON parse failure.
Shared Claude API entry point used by every agent in this module.
Sends `user_content` (optionally with a `system` prompt), parses the
response as JSON, and returns a dict. Raises RuntimeError on missing
library/key, API error, or JSON parse failure.
API key read from ir.config_parameter key 'fl_ai.claude_api_key'.
"""
try:
@@ -410,16 +416,19 @@ Respond with the JSON object only. No other text."""
'Set fl_ai.claude_api_key in Settings → Technical → System Parameters.'
)
_logger.info("FL AI Engine: calling Claude API (model=%s)", CLAUDE_MODEL)
_logger.info("FL AI: calling Claude API (model=%s)", CLAUDE_MODEL)
client = anthropic.Anthropic(api_key=api_key)
create_kwargs = {
'model': CLAUDE_MODEL,
'max_tokens': max_tokens,
'messages': [{'role': 'user', 'content': user_content}],
}
if system:
create_kwargs['system'] = system
try:
message = client.messages.create(
model=CLAUDE_MODEL,
max_tokens=2048,
messages=[{'role': 'user', 'content': prompt}],
)
message = client.messages.create(**create_kwargs)
except anthropic.RateLimitError as exc:
raise RuntimeError(f'Claude API rate limit exceeded: {exc}') from exc
except anthropic.APIConnectionError as exc:
@@ -427,10 +436,12 @@ Respond with the JSON object only. No other text."""
except anthropic.APIError as exc:
raise RuntimeError(f'Claude API error: {exc}') from exc
raw = message.content[0].text.strip()
_logger.debug("FL AI Engine raw response (first 200 chars): %s", raw[:200])
return self._extract_json(message.content[0].text.strip())
def _extract_json(self, raw):
"""Strip markdown fences / leading prose and parse the first JSON object."""
_logger.debug("FL AI raw response (first 200 chars): %s", raw[:200])
# Strip markdown code fences if present
if raw.startswith('```'):
parts = raw.split('```')
raw = parts[1] if len(parts) >= 2 else raw
@@ -438,7 +449,6 @@ Respond with the JSON object only. No other text."""
raw = raw[4:]
raw = raw.strip()
# Extract first JSON object if extra prose leaked through
if raw and raw[0] == '{':
brace_depth = 0
end_idx = 0
@@ -455,7 +465,7 @@ Respond with the JSON object only. No other text."""
try:
return json.loads(raw)
except json.JSONDecodeError as exc:
_logger.error("FL AI Engine: JSON parse error: %s\nRaw: %s", exc, raw[:500])
_logger.error("FL AI: JSON parse error: %s\nRaw: %s", exc, raw[:500])
raise RuntimeError(f"Claude returned invalid JSON: {exc}") from exc
# ──────────────────────────────────────────────────────────────────────────