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:
@@ -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
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user