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:
@@ -17,3 +17,4 @@ from . import fl_argument
|
||||
from . import fl_case
|
||||
from . import fl_conflict_check
|
||||
from . import fl_paralegal_agent
|
||||
from . import fl_attorney_agent
|
||||
|
||||
@@ -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
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -10,6 +10,10 @@ class FlAnalysis(models.Model):
|
||||
case_id = fields.Many2one(
|
||||
'fl.case', ondelete='cascade', index=True
|
||||
)
|
||||
analysis_type = fields.Selection([
|
||||
('engine', 'AI Engine Analysis'),
|
||||
('attorney', 'Attorney Strategy Memo'),
|
||||
], string='Analysis Type', default='engine')
|
||||
analysis_date = fields.Datetime(
|
||||
string='Analysis Date',
|
||||
default=fields.Datetime.now
|
||||
@@ -36,6 +40,23 @@ class FlAnalysis(models.Model):
|
||||
help='Resumen en español — sin jerga legal'
|
||||
)
|
||||
|
||||
# ── Attorney Strategy Memo ─────────────────────────────────────────────
|
||||
strategy_memo = fields.Html(
|
||||
string='Strategy Memo',
|
||||
help='Substantive strategy memo authored by the Attorney agent.'
|
||||
)
|
||||
risk_narrative = fields.Text(
|
||||
string='Risk Narrative',
|
||||
help='Narrative of substantive risks: DV, hidden assets, income '
|
||||
'imputation, unrepresented respondent, etc.'
|
||||
)
|
||||
cited_statute_ids = fields.Many2many(
|
||||
'fl.statute',
|
||||
'fl_analysis_statute_rel',
|
||||
'analysis_id', 'statute_id',
|
||||
string='Cited Statutes'
|
||||
)
|
||||
|
||||
# ── Analysis Detail (Phase 5) ──────────────────────────────────────────
|
||||
petitioner_arguments = fields.Text(
|
||||
string='Petitioner Arguments (JSON)'
|
||||
|
||||
345
activeblue_familylaw/models/fl_attorney_agent.py
Normal file
345
activeblue_familylaw/models/fl_attorney_agent.py
Normal file
@@ -0,0 +1,345 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from odoo import models
|
||||
|
||||
from .fl_ai_engine import CLAUDE_MODEL
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# Candidate library sizes passed to the model (it picks the top 3-5 from these).
|
||||
MAX_STATUTE_CANDIDATES = 12
|
||||
MAX_CASELAW_CANDIDATES = 12
|
||||
|
||||
ATTORNEY_SYSTEM_PROMPT = (
|
||||
"You are a senior Florida family-law attorney assistant for the 11th Judicial "
|
||||
"Circuit (Miami-Dade). You provide SUBSTANTIVE strategy support to the legal "
|
||||
"team — you are NOT giving legal advice to a litigant. Analyze the full case "
|
||||
"record and build on any prior analyses. "
|
||||
"Identify the strongest applicable statutes and case law, but you MUST choose "
|
||||
"ONLY from the candidate lists provided — never invent a citation. Draft "
|
||||
"arguments for the case's primary issues and counterarguments the opposing "
|
||||
"party is likely to raise. Surface substantive risks (domestic violence, "
|
||||
"hidden assets, income imputation, unrepresented respondent). For child-support "
|
||||
"modification, assess whether there is a substantial change of circumstances "
|
||||
"under FL 61.30(1)(b). Recommend attorney involvement when domestic violence, "
|
||||
"opposing counsel, or high complexity is present. "
|
||||
"Respond with a single JSON object only — no prose outside the JSON."
|
||||
)
|
||||
|
||||
|
||||
class FlAttorneyAgent(models.AbstractModel):
|
||||
"""
|
||||
Attorney AI agent — substantive legal reasoning.
|
||||
|
||||
Fires only on a deliberate user action (button on the case AI tab); never
|
||||
runs automatically. Produces a strategy memo stored as an fl.analysis record
|
||||
of type 'attorney', links the top statutes/case law from the existing
|
||||
library, drafts arguments, and writes a risk narrative. Falls back to a
|
||||
rule-based memo when the Claude API is unavailable — never surfaces a raw
|
||||
API error to the user.
|
||||
"""
|
||||
_name = 'fl.attorney.agent'
|
||||
_description = 'Attorney AI Agent (substantive)'
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Public entry point
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def generate_strategy_memo(self, case):
|
||||
"""Run a full substantive analysis. Returns the fl.analysis memo record."""
|
||||
analysis = self.env['fl.analysis'].create({
|
||||
'case_id': case.id,
|
||||
'analysis_type': 'attorney',
|
||||
'state': 'pending',
|
||||
'model_used': CLAUDE_MODEL,
|
||||
})
|
||||
|
||||
statutes = self._candidate_statutes(case)
|
||||
caselaw = self._candidate_caselaw(case)
|
||||
|
||||
try:
|
||||
context = self._build_context(case, statutes, caselaw)
|
||||
result = self.env['fl.ai.engine'].call_claude_json(
|
||||
user_content=(
|
||||
'Case record:\n' + json.dumps(context, indent=2)
|
||||
+ '\n\nProduce the strategy memo now.'
|
||||
),
|
||||
system=ATTORNEY_SYSTEM_PROMPT,
|
||||
max_tokens=3000,
|
||||
)
|
||||
self._store_memo(case, analysis, result, statutes, caselaw)
|
||||
except Exception as exc:
|
||||
_logger.error("Attorney agent failed for case %s: %s",
|
||||
case.id, exc, exc_info=True)
|
||||
self._store_fallback(case, analysis, statutes, caselaw, str(exc))
|
||||
|
||||
return analysis
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Candidate library (grounds the model in real records)
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _candidate_statutes(self, case):
|
||||
"""Statutes relevant to the case's issue tags + case type."""
|
||||
statutes = self.env['fl.paralegal.agent']._cross_reference_statutes(case)
|
||||
return statutes[:MAX_STATUTE_CANDIDATES]
|
||||
|
||||
def _candidate_caselaw(self, case):
|
||||
"""Case law tagged with any of the case's active issue tags."""
|
||||
if not case.issue_tag_ids:
|
||||
return self.env['fl.caselaw'].search(
|
||||
[('active', '=', True)], order='year desc',
|
||||
limit=MAX_CASELAW_CANDIDATES)
|
||||
return self.env['fl.caselaw'].search([
|
||||
('active', '=', True),
|
||||
('issue_tag_ids', 'in', case.issue_tag_ids.ids),
|
||||
], limit=MAX_CASELAW_CANDIDATES)
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Context
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _build_context(self, case, statutes, caselaw):
|
||||
pet = case.party_ids.filtered(lambda p: p.role == 'petitioner')[:1]
|
||||
resp = case.party_ids.filtered(lambda p: p.role == 'respondent')[:1]
|
||||
|
||||
def _party(party):
|
||||
if not party:
|
||||
return None
|
||||
return {
|
||||
'employment_type': party.employment_type,
|
||||
'gross_monthly_income': party.gross_monthly_income or 0,
|
||||
'net_monthly_income': party.net_monthly_income or 0,
|
||||
'effective_monthly_income': party.effective_monthly_income or 0,
|
||||
'income_imputed': party.income_imputed,
|
||||
'lifestyle_inconsistency': party.lifestyle_inconsistency_flag,
|
||||
}
|
||||
|
||||
# Prior completed analyses (the just-created memo is still 'pending',
|
||||
# so it is excluded) — lets the agent build on earlier work.
|
||||
prior = case.analysis_ids.filtered(
|
||||
lambda a: a.state == 'complete'
|
||||
).sorted('create_date', reverse=True)[:3]
|
||||
|
||||
return {
|
||||
'case_type': case.case_type,
|
||||
'stage': case.stage_id.name if case.stage_id else 'unknown',
|
||||
'complexity': (
|
||||
case.latest_analysis_id.case_complexity
|
||||
or self.env['fl.ai.engine']._fallback_complexity(case)
|
||||
),
|
||||
'issue_tags': case.issue_tag_ids.mapped('name'),
|
||||
'petitioner': _party(pet),
|
||||
'respondent': _party(resp),
|
||||
'respondent_has_counsel': case.respondent_has_counsel,
|
||||
'respondent_answered': case.respondent_answered,
|
||||
'children': [
|
||||
{'age': c.age, 'approaching_emancipation': c.approaching_emancipation}
|
||||
for c in case.child_ids if not c.emancipated
|
||||
],
|
||||
'support': {
|
||||
'current_order_total': case.current_order_total or 0,
|
||||
'calculated_support': case.calculated_support or 0,
|
||||
'support_difference': case.support_difference or 0,
|
||||
'support_difference_pct': round(case.support_difference_pct or 0, 1),
|
||||
'threshold_met': case.threshold_met,
|
||||
'substantial_change_basis': case.substantial_change_basis or None,
|
||||
'substantial_change_detail': case.substantial_change_detail or None,
|
||||
},
|
||||
'timesharing': {
|
||||
'petitioner_overnights': case.petitioner_overnights or 0,
|
||||
'respondent_overnights': case.respondent_overnights or 0,
|
||||
'substantial_timesharing': case.substantial_timesharing_applies,
|
||||
},
|
||||
'flags': {
|
||||
'domestic_violence': case.domestic_violence_flag,
|
||||
'dv_injunction_active': case.dv_injunction_active,
|
||||
'residency_met': case.residency_requirement_met,
|
||||
'fee_waiver_eligible': case.fee_waiver_eligible,
|
||||
},
|
||||
'prior_analyses': [
|
||||
{'date': str(a.analysis_date), 'type': a.analysis_type,
|
||||
'summary': a.plain_english_summary or ''}
|
||||
for a in prior
|
||||
],
|
||||
'candidate_statutes': [
|
||||
{'name': s.name, 'title': s.title, 'category': s.category}
|
||||
for s in statutes
|
||||
],
|
||||
'candidate_caselaw': [
|
||||
{'citation': c.citation, 'holding': (c.holding or '')[:300],
|
||||
'favorable_to': c.favorable_to}
|
||||
for c in caselaw
|
||||
],
|
||||
'output_schema': {
|
||||
'strategy_memo': 'detailed strategy memo (markdown allowed)',
|
||||
'plain_english_summary': '3-5 sentence summary, no jargon',
|
||||
'plain_english_summary_es': 'same summary in Spanish',
|
||||
'top_statutes': 'list of 3-5 exact statute names from candidate_statutes',
|
||||
'top_caselaw': 'list of 3-5 exact citations from candidate_caselaw',
|
||||
'petitioner_arguments': 'list of strings',
|
||||
'respondent_counterarguments': 'list of strings',
|
||||
'procedural_risks': 'list of strings',
|
||||
'risk_narrative': 'substantive risk narrative string',
|
||||
'substantial_change_detected': 'true/false (modification cases)',
|
||||
'substantial_change_narrative': 'string',
|
||||
'attorney_referral_flag': 'true/false',
|
||||
'attorney_referral_reason': 'string or null',
|
||||
'confidence_level': 'high|medium|low',
|
||||
'case_complexity': 'simple|moderate|complex',
|
||||
},
|
||||
}
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Store (AI result)
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _store_memo(self, case, analysis, result, statutes, caselaw):
|
||||
# Resolve the model's picks back to real records (only from candidates)
|
||||
picked_statutes = statutes.filtered(
|
||||
lambda s: s.name in (result.get('top_statutes') or [])
|
||||
) or statutes[:5]
|
||||
picked_caselaw = caselaw.filtered(
|
||||
lambda c: c.citation in (result.get('top_caselaw') or [])
|
||||
) or caselaw[:5]
|
||||
|
||||
attorney_flag = result.get('attorney_referral_flag', False)
|
||||
if isinstance(attorney_flag, str):
|
||||
attorney_flag = attorney_flag.lower() in ('true', '1', 'yes')
|
||||
|
||||
memo_html = self._memo_to_html(result, picked_statutes, picked_caselaw)
|
||||
|
||||
analysis.write({
|
||||
'state': 'complete',
|
||||
'strategy_memo': memo_html,
|
||||
'plain_english_summary': result.get('plain_english_summary', ''),
|
||||
'plain_english_summary_es': result.get('plain_english_summary_es', ''),
|
||||
'petitioner_arguments': json.dumps(
|
||||
result.get('petitioner_arguments', []), ensure_ascii=False, indent=2),
|
||||
'respondent_counterarguments': json.dumps(
|
||||
result.get('respondent_counterarguments', []), ensure_ascii=False, indent=2),
|
||||
'procedural_risks': json.dumps(
|
||||
result.get('procedural_risks', []), ensure_ascii=False, indent=2),
|
||||
'risk_narrative': result.get('risk_narrative', ''),
|
||||
'attorney_referral_flag': bool(attorney_flag),
|
||||
'attorney_referral_reason': result.get('attorney_referral_reason') or '',
|
||||
'confidence_level': result.get('confidence_level', 'medium'),
|
||||
'case_complexity': result.get('case_complexity', 'moderate'),
|
||||
'cited_statute_ids': [(6, 0, picked_statutes.ids)],
|
||||
'matched_caselaw_ids': [(6, 0, picked_caselaw.ids)],
|
||||
'raw_response': json.dumps(result, ensure_ascii=False, indent=2),
|
||||
})
|
||||
|
||||
self._link_and_announce(case, analysis, picked_caselaw, attorney_flag,
|
||||
result.get('attorney_referral_reason'))
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Store (rule-based fallback — no AI)
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _store_fallback(self, case, analysis, statutes, caselaw, error):
|
||||
complexity = self.env['fl.ai.engine']._fallback_complexity(case)
|
||||
picked_statutes = statutes[:5]
|
||||
picked_caselaw = caselaw[:5]
|
||||
|
||||
risks = []
|
||||
if case.domestic_violence_flag:
|
||||
risks.append('Domestic violence flagged — affects mediation, '
|
||||
'timesharing, and safety; attorney strongly advised.')
|
||||
if case.respondent_has_counsel:
|
||||
risks.append('Respondent has counsel — pro se petitioner is at a '
|
||||
'significant disadvantage.')
|
||||
if any(p.income_imputed or p.lifestyle_inconsistency_flag
|
||||
for p in case.party_ids):
|
||||
risks.append('Possible income imputation / lifestyle inconsistency '
|
||||
'(FL 61.30(2)(b)).')
|
||||
if case.case_type == 'modification' and not case.threshold_met:
|
||||
risks.append('Modification threshold (FL 61.30(1)(b)) not currently met.')
|
||||
if not case.residency_requirement_met:
|
||||
risks.append('Residency requirement (FL 61.021) not yet verified.')
|
||||
|
||||
attorney_flag = bool(
|
||||
case.domestic_violence_flag
|
||||
or case.respondent_has_counsel
|
||||
or complexity == 'complex'
|
||||
)
|
||||
substantial_change = (
|
||||
case.case_type == 'modification' and case.threshold_met
|
||||
)
|
||||
|
||||
memo_html = (
|
||||
'<p><i>Rule-based strategy memo (Claude API unavailable).</i></p>'
|
||||
f'<p><b>Case type:</b> {case.case_type} '
|
||||
f'<b>Complexity:</b> {complexity}</p>'
|
||||
'<p><b>Applicable statutes:</b> '
|
||||
+ (', '.join(picked_statutes.mapped('name')) or '—') + '</p>'
|
||||
'<p><b>Relevant case law:</b> '
|
||||
+ (', '.join(picked_caselaw.mapped('citation')) or '—') + '</p>'
|
||||
'<p><b>Substantial change of circumstances (FL 61.30(1)(b)):</b> '
|
||||
+ ('Likely — threshold met.' if substantial_change
|
||||
else 'Not established on current figures.') + '</p>'
|
||||
'<p><b>Risks:</b></p><ul>'
|
||||
+ (''.join(f'<li>{r}</li>' for r in risks) or '<li>None flagged.</li>')
|
||||
+ '</ul>'
|
||||
)
|
||||
|
||||
analysis.write({
|
||||
'state': 'complete',
|
||||
'strategy_memo': memo_html,
|
||||
'risk_narrative': '\n'.join(risks),
|
||||
'plain_english_summary': (
|
||||
'Rule-based strategy summary generated without AI. '
|
||||
'Review statutes, case law, and risks above.'
|
||||
),
|
||||
'attorney_referral_flag': attorney_flag,
|
||||
'attorney_referral_reason': (
|
||||
'; '.join(risks) if attorney_flag else ''
|
||||
),
|
||||
'confidence_level': 'low',
|
||||
'case_complexity': complexity,
|
||||
'cited_statute_ids': [(6, 0, picked_statutes.ids)],
|
||||
'matched_caselaw_ids': [(6, 0, picked_caselaw.ids)],
|
||||
'error_message': error,
|
||||
})
|
||||
|
||||
self._link_and_announce(case, analysis, picked_caselaw, attorney_flag,
|
||||
analysis.attorney_referral_reason)
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Shared finalization
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _link_and_announce(self, case, analysis, caselaw, attorney_flag, reason):
|
||||
vals = {'attorney_memo_id': analysis.id}
|
||||
if caselaw:
|
||||
vals['caselaw_ids'] = [(4, c.id) for c in caselaw]
|
||||
case.write(vals)
|
||||
|
||||
body = ('<b>⚖️ Attorney Strategy Memo</b><br/>'
|
||||
+ (analysis.plain_english_summary or 'Strategy memo generated.'))
|
||||
if attorney_flag:
|
||||
body += (
|
||||
'<br/><br/><b style="color:#dc3545;">⚠️ Attorney involvement '
|
||||
'recommended:</b> ' + (reason or 'High complexity / risk.')
|
||||
)
|
||||
case.message_post(body=body, subtype_xmlid='mail.mt_comment')
|
||||
|
||||
def _memo_to_html(self, result, statutes, caselaw):
|
||||
def _list(items):
|
||||
return ''.join(f'<li>{i}</li>' for i in (items or [])) or '<li>—</li>'
|
||||
|
||||
memo = (result.get('strategy_memo') or '').replace('\n', '<br/>')
|
||||
return (
|
||||
f'<div>{memo}</div>'
|
||||
'<hr/><p><b>Top statutes:</b> '
|
||||
+ (', '.join(statutes.mapped('name')) or '—') + '</p>'
|
||||
'<p><b>Top case law:</b> '
|
||||
+ (', '.join(caselaw.mapped('citation')) or '—') + '</p>'
|
||||
'<p><b>Petitioner arguments:</b></p><ul>'
|
||||
+ _list(result.get('petitioner_arguments')) + '</ul>'
|
||||
'<p><b>Respondent counterarguments:</b></p><ul>'
|
||||
+ _list(result.get('respondent_counterarguments')) + '</ul>'
|
||||
'<p><b>Substantial change (FL 61.30(1)(b)):</b> '
|
||||
+ (result.get('substantial_change_narrative') or '—') + '</p>'
|
||||
)
|
||||
@@ -434,6 +434,19 @@ class FlCase(models.Model):
|
||||
string='Latest Analysis',
|
||||
compute='_compute_latest_analysis', store=True
|
||||
)
|
||||
attorney_memo_id = fields.Many2one(
|
||||
'fl.analysis',
|
||||
string='Attorney Strategy Memo',
|
||||
help='Current strategy memo produced by the Attorney agent.'
|
||||
)
|
||||
attorney_memo_html = fields.Html(
|
||||
string='Strategy Memo',
|
||||
related='attorney_memo_id.strategy_memo'
|
||||
)
|
||||
attorney_risk_narrative = fields.Text(
|
||||
string='Risk Narrative',
|
||||
related='attorney_memo_id.risk_narrative'
|
||||
)
|
||||
attorney_referral_flag = fields.Boolean(
|
||||
string='Attorney Referral Recommended',
|
||||
compute='_compute_attorney_referral_flag', store=True
|
||||
@@ -1159,3 +1172,15 @@ class FlCase(models.Model):
|
||||
'sticky': False,
|
||||
},
|
||||
}
|
||||
|
||||
def action_run_attorney_agent(self):
|
||||
self.ensure_one()
|
||||
memo = self.env['fl.attorney.agent'].generate_strategy_memo(self)
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'Attorney Strategy Memo',
|
||||
'res_model': 'fl.analysis',
|
||||
'res_id': memo.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'current',
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<field name="arch" type="xml">
|
||||
<tree string="AI Analyses">
|
||||
<field name="case_id"/>
|
||||
<field name="analysis_type"/>
|
||||
<field name="analysis_date"/>
|
||||
<field name="model_used"/>
|
||||
<field name="attorney_referral_flag"/>
|
||||
@@ -27,6 +28,7 @@
|
||||
<group>
|
||||
<group>
|
||||
<field name="case_id"/>
|
||||
<field name="analysis_type"/>
|
||||
<field name="analysis_date"/>
|
||||
<field name="model_used"/>
|
||||
<field name="state"/>
|
||||
@@ -42,10 +44,22 @@
|
||||
<strong>⚠️ ATTORNEY REFERRAL RECOMMENDED</strong><br/>
|
||||
<field name="attorney_referral_reason" readonly="1" nolabel="1"/>
|
||||
</div>
|
||||
<separator string="Strategy Memo"
|
||||
attrs="{'invisible': [('analysis_type', '!=', 'attorney')]}"/>
|
||||
<field name="strategy_memo" widget="html" readonly="1" nolabel="1"
|
||||
attrs="{'invisible': [('analysis_type', '!=', 'attorney')]}"/>
|
||||
<separator string="Risk Narrative"
|
||||
attrs="{'invisible': [('risk_narrative', '=', False)]}"/>
|
||||
<field name="risk_narrative" readonly="1" nolabel="1"
|
||||
attrs="{'invisible': [('risk_narrative', '=', False)]}"/>
|
||||
<separator string="Plain English Summary (English)"/>
|
||||
<field name="plain_english_summary" readonly="1" nolabel="1"/>
|
||||
<separator string="Plain English Summary (Español)"/>
|
||||
<field name="plain_english_summary_es" readonly="1" nolabel="1"/>
|
||||
<separator string="Cited Statutes"
|
||||
attrs="{'invisible': [('cited_statute_ids', '=', [])]}"/>
|
||||
<field name="cited_statute_ids" widget="many2many_tags" readonly="1" nolabel="1"
|
||||
attrs="{'invisible': [('cited_statute_ids', '=', [])]}"/>
|
||||
<separator string="Matched Case Law"/>
|
||||
<field name="matched_caselaw_ids" widget="many2many_tags" readonly="1" nolabel="1"/>
|
||||
</sheet>
|
||||
|
||||
@@ -266,10 +266,22 @@
|
||||
placeholder="Run AI Analysis to generate summary"/>
|
||||
<separator string="AI Case Summary (Español)"/>
|
||||
<field name="ai_plain_english_es" readonly="1" nolabel="1"/>
|
||||
<separator string="Attorney Strategy Memo"/>
|
||||
<button name="action_run_attorney_agent"
|
||||
string="Generate Attorney Strategy Memo"
|
||||
type="object" class="btn-primary"
|
||||
groups="activeblue_familylaw.group_admin"/>
|
||||
<field name="attorney_memo_id" readonly="1"
|
||||
attrs="{'invisible': [('attorney_memo_id', '=', False)]}"/>
|
||||
<field name="attorney_memo_html" widget="html" readonly="1" nolabel="1"
|
||||
attrs="{'invisible': [('attorney_memo_id', '=', False)]}"/>
|
||||
<field name="attorney_risk_narrative" readonly="1"
|
||||
attrs="{'invisible': [('attorney_memo_id', '=', False)]}"/>
|
||||
<separator string="All Analyses"/>
|
||||
<field name="analysis_ids">
|
||||
<tree string="Analyses">
|
||||
<field name="analysis_date"/>
|
||||
<field name="analysis_type"/>
|
||||
<field name="model_used"/>
|
||||
<field name="attorney_referral_flag"/>
|
||||
<field name="confidence_level"/>
|
||||
|
||||
Reference in New Issue
Block a user