Migrate AI engine to Claude API; convert stage field to Many2one Kanban model
- Replace fl.case.stage Selection field with Many2one → fl.case.stage model, enabling Kanban grouping and dynamic stage management - Add FlCaseStage model (sequence, fold, description) and fl_stage_data.xml with all 11 procedural stages seeded with noupdate=1 - Migrate fl_ai_engine.py from Ollama/llama3.1 to Claude API (claude-sonnet-4-20250514); key from ir.config_parameter fl_ai.claude_api_key - Fix stale field references in _rule_based_tagging and _build_case_context: employment/income now read from party_ids, timesharing fields corrected - Add _fallback_complexity() for graceful degradation when API unavailable - Add Kanban view to fl_case_views.xml; update action view_mode to kanban,tree,form - Add fl.case.stage ACL entries to ir.model.access.csv Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,39 +5,27 @@ from odoo import models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
OLLAMA_URL = 'http://192.168.2.10:11434/api/generate'
|
||||
OLLAMA_MODEL = 'llama3.1'
|
||||
CLAUDE_MODEL = 'claude-sonnet-4-20250514'
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Rule-based issue tag weights
|
||||
# Maps (field_name, value_or_True) → list of issue tag XML ids
|
||||
# Maps (field_name, value_or_True) → list of issue tag names
|
||||
# Applied to fl.case fields directly (employment-based rules handled separately
|
||||
# via party_ids lookup in _rule_based_tagging).
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
ISSUE_RULES = [
|
||||
# Modification threshold
|
||||
CASE_FIELD_RULES = [
|
||||
('threshold_met', True, ['modification_threshold']),
|
||||
# Income imputation triggers
|
||||
('respondent_employment_status', 'unemployed', ['income_imputation']),
|
||||
('respondent_employment_status', 'self_employed', ['self_employment_income', 'income_imputation']),
|
||||
('petitioner_employment_status', 'self_employed', ['self_employment_income']),
|
||||
# Timesharing deviation
|
||||
('substantial_timesharing', True, ['timesharing_deviation']),
|
||||
# Domestic violence
|
||||
('substantial_timesharing_applies', True, ['timesharing_deviation']),
|
||||
('domestic_violence_flag', True, ['domestic_violence']),
|
||||
# Fee waiver
|
||||
('fee_waiver_eligible', True, ['fee_waiver']),
|
||||
# Default judgment track
|
||||
('respondent_answered', False, ['default_judgment']),
|
||||
# Residency
|
||||
('residency_requirement_met', False, ['residency']),
|
||||
# Parenting class
|
||||
('parenting_class_required', True, ['parenting_class']),
|
||||
# Post-order
|
||||
('case_type', 'modification', ['post_order']),
|
||||
]
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Caselaw topic → issue tag matching
|
||||
# Used by _match_caselaw to find relevant cases
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
TAG_TO_CASELAW_DOMAINS = {
|
||||
'modification_threshold': [('issue_tag_ids.name', '=', 'modification_threshold')],
|
||||
@@ -52,36 +40,33 @@ TAG_TO_CASELAW_DOMAINS = {
|
||||
'post_order': [('issue_tag_ids.name', '=', 'post_order')],
|
||||
}
|
||||
|
||||
# Maximum caselaw records to pass to Ollama (keep prompt size manageable)
|
||||
MAX_CASELAW_IN_PROMPT = 8
|
||||
|
||||
|
||||
class FlAiEngine(models.AbstractModel):
|
||||
"""
|
||||
Phase 5 — Full Ollama integration with rule-based pre-processing.
|
||||
AI Analysis Engine — Claude API (claude-sonnet-4-20250514).
|
||||
|
||||
Workflow:
|
||||
1. _rule_based_tagging — tag issue tags from case field values (fast, deterministic)
|
||||
2. _match_caselaw — find relevant FL cases from the caselaw library
|
||||
3. _build_case_context — serialize case data to a JSON dict
|
||||
4. _build_prompt — compose the Ollama prompt
|
||||
5. _call_ollama — HTTP call to Ollama with error handling
|
||||
4. _build_prompt — compose the Claude prompt
|
||||
5. _call_claude — Claude API call with error handling
|
||||
6. _store_analysis — persist fl.analysis record with results
|
||||
|
||||
Falls back to _fallback_complexity() when Claude API is unavailable.
|
||||
AbstractModel — not stored in the database.
|
||||
"""
|
||||
_name = 'fl.ai.engine'
|
||||
_description = 'Family Law AI Analysis Engine (Ollama)'
|
||||
_description = 'Family Law AI Analysis Engine (Claude API)'
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Public entry point
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def analyze_case(self, case_id):
|
||||
"""
|
||||
Full Phase 5 analysis entry point.
|
||||
Returns fl.analysis record.
|
||||
"""
|
||||
"""Full analysis entry point. Returns fl.analysis record."""
|
||||
case = self.env['fl.case'].browse(case_id)
|
||||
if not case.exists():
|
||||
raise ValueError(f"Case {case_id} not found")
|
||||
@@ -89,45 +74,35 @@ class FlAiEngine(models.AbstractModel):
|
||||
analysis_vals = {
|
||||
'case_id': case.id,
|
||||
'state': 'pending',
|
||||
'model_used': OLLAMA_MODEL,
|
||||
'model_used': CLAUDE_MODEL,
|
||||
}
|
||||
analysis = self.env['fl.analysis'].create(analysis_vals)
|
||||
|
||||
try:
|
||||
# Step 1: Rule-based tagging
|
||||
triggered_tags = self._rule_based_tagging(case)
|
||||
if triggered_tags:
|
||||
existing_tags = case.issue_tag_ids.mapped('name')
|
||||
new_tag_recs = self.env['fl.issue.tag'].search([
|
||||
('name', 'in', triggered_tags),
|
||||
('name', 'in', list(triggered_tags)),
|
||||
('name', 'not in', existing_tags),
|
||||
])
|
||||
if new_tag_recs:
|
||||
case.write({'issue_tag_ids': [(4, t.id) for t in new_tag_recs]})
|
||||
|
||||
# Step 2: Match caselaw
|
||||
matched_cases = self._match_caselaw(triggered_tags)
|
||||
|
||||
# Step 3: Build case context
|
||||
context = self._build_case_context(case, matched_cases)
|
||||
|
||||
# Step 4: Determine complexity (used in prompt and result)
|
||||
complexity = self._assess_complexity(case, triggered_tags)
|
||||
|
||||
# Step 5: Build prompt
|
||||
prompt = self._build_prompt(context, complexity)
|
||||
|
||||
# Step 6: Call Ollama
|
||||
result = self._call_ollama(prompt)
|
||||
|
||||
# Step 7: Store results
|
||||
result = self._call_claude(prompt)
|
||||
self._store_analysis(analysis, result, matched_cases, complexity)
|
||||
|
||||
except Exception as exc:
|
||||
_logger.error("AI analysis failed for case %s: %s", case_id, exc, exc_info=True)
|
||||
fallback_complexity = self._fallback_complexity(case)
|
||||
analysis.write({
|
||||
'state': 'failed',
|
||||
'error_message': str(exc),
|
||||
'case_complexity': fallback_complexity,
|
||||
'plain_english_summary': (
|
||||
"AI analysis could not be completed at this time. "
|
||||
"Please try again later or contact support."
|
||||
@@ -146,38 +121,46 @@ class FlAiEngine(models.AbstractModel):
|
||||
|
||||
def _rule_based_tagging(self, case):
|
||||
"""
|
||||
Apply deterministic rules to identify legal issues present in the case.
|
||||
Apply deterministic rules to identify legal issues in the case.
|
||||
Returns a set of issue tag name strings that were triggered.
|
||||
"""
|
||||
triggered = set()
|
||||
|
||||
for rule in ISSUE_RULES:
|
||||
field_name, expected, tags = rule
|
||||
if not hasattr(case, field_name):
|
||||
continue
|
||||
val = getattr(case, field_name)
|
||||
# Handle relational fields (Many2one returns recordset)
|
||||
if hasattr(val, '_name'):
|
||||
continue
|
||||
# Direct case field checks
|
||||
for field_name, expected, tags in CASE_FIELD_RULES:
|
||||
val = getattr(case, field_name, None)
|
||||
if val == expected:
|
||||
triggered.update(tags)
|
||||
|
||||
# Additional logic: income imputation if large discrepancy
|
||||
if (case.petitioner_net_income and case.respondent_net_income
|
||||
and case.respondent_employment_status not in ('unemployed', 'self_employed')):
|
||||
pet = case.petitioner_net_income
|
||||
resp = case.respondent_net_income
|
||||
if resp > 0 and pet > 0:
|
||||
ratio = max(pet, resp) / min(pet, resp)
|
||||
# Employment-based checks from party_ids
|
||||
petitioner_party = case.party_ids.filtered(lambda p: p.role == 'petitioner')
|
||||
respondent_party = case.party_ids.filtered(lambda p: p.role == 'respondent')
|
||||
pet = petitioner_party[0] if petitioner_party else None
|
||||
resp = respondent_party[0] if respondent_party else None
|
||||
|
||||
if resp:
|
||||
if resp.employment_type == 'unemployed':
|
||||
triggered.add('income_imputation')
|
||||
elif resp.employment_type in ('self_employed', 'underemployed'):
|
||||
triggered.update(['self_employment_income', 'income_imputation'])
|
||||
if pet and pet.employment_type == 'self_employed':
|
||||
triggered.add('self_employment_income')
|
||||
|
||||
# Large income disparity → possible income imputation issue
|
||||
if pet and resp:
|
||||
pet_income = pet.effective_monthly_income or 0
|
||||
resp_income = resp.effective_monthly_income or 0
|
||||
if (pet_income > 0 and resp_income > 0
|
||||
and resp.employment_type not in ('unemployed', 'self_employed', 'underemployed')):
|
||||
ratio = max(pet_income, resp_income) / min(pet_income, resp_income)
|
||||
if ratio > 3.0:
|
||||
triggered.add('income_imputation')
|
||||
|
||||
# Emancipation approaching
|
||||
if case.child_ids:
|
||||
for child in case.child_ids:
|
||||
if child.approaching_emancipation:
|
||||
triggered.add('post_order')
|
||||
break
|
||||
# Approaching emancipation → post_order concern
|
||||
for child in case.child_ids:
|
||||
if child.approaching_emancipation:
|
||||
triggered.add('post_order')
|
||||
break
|
||||
|
||||
return triggered
|
||||
|
||||
@@ -187,20 +170,17 @@ class FlAiEngine(models.AbstractModel):
|
||||
|
||||
def _match_caselaw(self, triggered_tags):
|
||||
"""
|
||||
Search the fl.caselaw library for cases matching the triggered issue tags.
|
||||
Prioritizes 3rd DCA cases (Miami-Dade's primary appellate court),
|
||||
then FL Supreme Court, then other DCAs.
|
||||
Search the fl.caselaw library for cases matching triggered issue tags.
|
||||
Prioritizes 3rd DCA (Miami-Dade's appellate court), then FL Supreme Court.
|
||||
Returns up to MAX_CASELAW_IN_PROMPT records.
|
||||
"""
|
||||
if not triggered_tags:
|
||||
# Return a small set of foundational cases
|
||||
return self.env['fl.caselaw'].search(
|
||||
[('active', '=', True)],
|
||||
order='year desc',
|
||||
limit=4,
|
||||
)
|
||||
|
||||
# Collect matching case IDs per tag, with deduplication
|
||||
matched_ids = set()
|
||||
for tag in triggered_tags:
|
||||
domain = TAG_TO_CASELAW_DOMAINS.get(tag, [])
|
||||
@@ -214,23 +194,20 @@ class FlAiEngine(models.AbstractModel):
|
||||
if not matched_ids:
|
||||
return self.env['fl.caselaw'].browse()
|
||||
|
||||
# Fetch and sort: 3rd DCA first, then FL Supreme, then by year desc
|
||||
cases = self.env['fl.caselaw'].browse(list(matched_ids))
|
||||
court_priority = {
|
||||
'3rd_dca': 0,
|
||||
'fl_supreme': 1,
|
||||
'4th_dca': 2,
|
||||
'2nd_dca': 3,
|
||||
'1st_dca': 4,
|
||||
'5th_dca': 5,
|
||||
'11th_circuit': 6,
|
||||
'other': 7,
|
||||
'3rd_dca': 0, 'fl_supreme': 1,
|
||||
'4th_dca': 2, '2nd_dca': 3,
|
||||
'1st_dca': 4, '5th_dca': 5,
|
||||
'11th_circuit': 6, 'other': 7,
|
||||
}
|
||||
cases = self.env['fl.caselaw'].browse(list(matched_ids))
|
||||
sorted_cases = sorted(
|
||||
cases,
|
||||
key=lambda c: (court_priority.get(c.court, 9), -c.year)
|
||||
)
|
||||
return self.env['fl.caselaw'].browse([c.id for c in sorted_cases[:MAX_CASELAW_IN_PROMPT]])
|
||||
return self.env['fl.caselaw'].browse(
|
||||
[c.id for c in sorted_cases[:MAX_CASELAW_IN_PROMPT]]
|
||||
)
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Step 3: Build case context
|
||||
@@ -239,20 +216,25 @@ class FlAiEngine(models.AbstractModel):
|
||||
def _build_case_context(self, case, matched_cases):
|
||||
"""
|
||||
Serialize the case into a JSON-serializable dict for the prompt.
|
||||
Keeps PII minimal — uses role names, not actual SSNs etc.
|
||||
Keeps PII minimal — uses role names, not SSNs etc.
|
||||
"""
|
||||
petitioner_party = case.party_ids.filtered(lambda p: p.role == 'petitioner')
|
||||
respondent_party = case.party_ids.filtered(lambda p: p.role == 'respondent')
|
||||
pet = petitioner_party[0] if petitioner_party else None
|
||||
resp = respondent_party[0] if respondent_party else None
|
||||
|
||||
context = {
|
||||
'case_type': case.case_type,
|
||||
'stage': case.stage_id.name if case.stage_id else 'unknown',
|
||||
'filing_date': str(case.filing_date) if case.filing_date else None,
|
||||
'service_date': str(case.service_date) if case.service_date else None,
|
||||
'petitioner': {
|
||||
'employment': case.petitioner_id.employment_status if case.petitioner_id else 'unknown',
|
||||
'monthly_net_income': case.petitioner_net_income or 0,
|
||||
'employment': pet.employment_type if pet else 'unknown',
|
||||
'monthly_net_income': pet.effective_monthly_income if pet else 0,
|
||||
},
|
||||
'respondent': {
|
||||
'employment': case.respondent_id.employment_status if case.respondent_id else 'unknown',
|
||||
'monthly_net_income': case.respondent_net_income or 0,
|
||||
'employment': resp.employment_type if resp else 'unknown',
|
||||
'monthly_net_income': resp.effective_monthly_income if resp else 0,
|
||||
'answered': case.respondent_answered,
|
||||
'has_counsel': case.respondent_has_counsel,
|
||||
},
|
||||
@@ -268,21 +250,26 @@ class FlAiEngine(models.AbstractModel):
|
||||
{
|
||||
'age': c.age,
|
||||
'approaching_emancipation': c.approaching_emancipation,
|
||||
'days_until_emancipation': c.days_until_emancipation if c.approaching_emancipation else None,
|
||||
'days_until_emancipation': (
|
||||
c.days_until_emancipation if c.approaching_emancipation else None
|
||||
),
|
||||
}
|
||||
for c in (case.child_ids or [])
|
||||
if not c.emancipated
|
||||
],
|
||||
'timesharing': {
|
||||
'petitioner_overnights': case.petitioner_overnights_year or 0,
|
||||
'respondent_overnights': case.respondent_overnights_year or 0,
|
||||
'substantial_timesharing': case.substantial_timesharing,
|
||||
'petitioner_overnights': case.petitioner_overnights or 0,
|
||||
'respondent_overnights': case.respondent_overnights or 0,
|
||||
'substantial_timesharing': case.substantial_timesharing_applies,
|
||||
},
|
||||
'residency_met': case.residency_requirement_met,
|
||||
'domestic_violence': case.domestic_violence_flag,
|
||||
'fee_waiver_eligible': case.fee_waiver_eligible,
|
||||
'parenting_class_required': case.parenting_class_required,
|
||||
'parenting_class_completed': case.parenting_class_completed,
|
||||
'parenting_class_completed': (
|
||||
case.petitioner_parenting_class_done
|
||||
and case.respondent_parenting_class_done
|
||||
),
|
||||
'issue_tags': list(case.issue_tag_ids.mapped('name')),
|
||||
'caselaw': [
|
||||
{
|
||||
@@ -300,10 +287,7 @@ class FlAiEngine(models.AbstractModel):
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _assess_complexity(self, case, triggered_tags):
|
||||
"""
|
||||
Simple heuristic complexity scorer.
|
||||
Returns 'simple', 'moderate', or 'complex'.
|
||||
"""
|
||||
"""Simple heuristic complexity scorer. Returns 'simple', 'moderate', or 'complex'."""
|
||||
score = 0
|
||||
if case.domestic_violence_flag:
|
||||
score += 3
|
||||
@@ -325,15 +309,34 @@ class FlAiEngine(models.AbstractModel):
|
||||
else:
|
||||
return 'complex'
|
||||
|
||||
def _fallback_complexity(self, case):
|
||||
"""
|
||||
Rule-based complexity fallback used when Claude API is unavailable.
|
||||
Returns 'simple', 'moderate', or 'complex'.
|
||||
"""
|
||||
score = 0
|
||||
if case.domestic_violence_flag:
|
||||
score += 3
|
||||
if case.respondent_has_counsel:
|
||||
score += 2
|
||||
if case.child_ids and len(case.child_ids) > 2:
|
||||
score += 1
|
||||
if score <= 1:
|
||||
return 'simple'
|
||||
elif score <= 4:
|
||||
return 'moderate'
|
||||
else:
|
||||
return 'complex'
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Step 5: Build Ollama prompt
|
||||
# Step 5: Build prompt
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _build_prompt(self, context, complexity):
|
||||
"""
|
||||
Construct the system + user prompt for Ollama.
|
||||
Returns a string.
|
||||
The LLM is instructed to respond with a JSON object only.
|
||||
Construct the prompt for Claude.
|
||||
Returns a string (user message content).
|
||||
The model is instructed to respond with a JSON object only.
|
||||
"""
|
||||
context_json = json.dumps(context, indent=2)
|
||||
|
||||
@@ -364,7 +367,7 @@ TASK: Analyze this case and return a JSON object with exactly these fields:
|
||||
"petitioner_arguments": ["argument 1", "argument 2", "argument 3"],
|
||||
"respondent_counterarguments": ["counterargument 1", "counterargument 2"],
|
||||
"procedural_risks": ["risk 1", "risk 2"],
|
||||
"attorney_referral_flag": {"attorney_trigger" if attorney_trigger else "false"},
|
||||
"attorney_referral_flag": {"true" if attorney_trigger else "false"},
|
||||
"attorney_referral_reason": "reason if flag is true, else null",
|
||||
"confidence_level": "high|medium|low",
|
||||
"case_complexity": "{complexity}",
|
||||
@@ -376,55 +379,58 @@ Respond with the JSON object only. No other text."""
|
||||
return prompt
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Step 6: Call Ollama
|
||||
# Step 6: Call Claude API
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _call_ollama(self, prompt):
|
||||
def _call_claude(self, prompt):
|
||||
"""
|
||||
Call Ollama API (llama3.1) and return parsed JSON dict.
|
||||
Raises on network error or JSON parse failure.
|
||||
Call Claude API and return parsed JSON dict.
|
||||
Raises RuntimeError on connection error or JSON parse failure.
|
||||
API key read from ir.config_parameter key 'fl_ai.claude_api_key'.
|
||||
"""
|
||||
try:
|
||||
import requests
|
||||
import anthropic
|
||||
except ImportError:
|
||||
raise RuntimeError(
|
||||
'requests library not available. '
|
||||
'Install with: pip install requests'
|
||||
'anthropic library not installed. Run: pip install anthropic'
|
||||
)
|
||||
|
||||
_logger.info("FL AI Engine: calling Ollama at %s (model=%s)", OLLAMA_URL, OLLAMA_MODEL)
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('fl_ai.claude_api_key')
|
||||
if not api_key:
|
||||
raise RuntimeError(
|
||||
'Claude API key not configured. '
|
||||
'Set fl_ai.claude_api_key in Settings → Technical → System Parameters.'
|
||||
)
|
||||
|
||||
response = requests.post(
|
||||
OLLAMA_URL,
|
||||
json={
|
||||
'model': OLLAMA_MODEL,
|
||||
'prompt': prompt,
|
||||
'stream': False,
|
||||
'options': {
|
||||
'temperature': 0.1,
|
||||
'top_p': 0.9,
|
||||
'num_predict': 2000,
|
||||
},
|
||||
},
|
||||
timeout=180,
|
||||
)
|
||||
response.raise_for_status()
|
||||
_logger.info("FL AI Engine: calling Claude API (model=%s)", CLAUDE_MODEL)
|
||||
|
||||
raw = response.json().get('response', '{}').strip()
|
||||
client = anthropic.Anthropic(api_key=api_key)
|
||||
|
||||
try:
|
||||
message = client.messages.create(
|
||||
model=CLAUDE_MODEL,
|
||||
max_tokens=2048,
|
||||
messages=[{'role': 'user', 'content': prompt}],
|
||||
)
|
||||
except anthropic.RateLimitError as exc:
|
||||
raise RuntimeError(f'Claude API rate limit exceeded: {exc}') from exc
|
||||
except anthropic.APIConnectionError as exc:
|
||||
raise RuntimeError(f'Claude API connection error: {exc}') from exc
|
||||
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])
|
||||
|
||||
# Strip markdown code fences if present
|
||||
if raw.startswith('```'):
|
||||
parts = raw.split('```')
|
||||
if len(parts) >= 3:
|
||||
raw = parts[1]
|
||||
elif len(parts) == 2:
|
||||
raw = parts[1]
|
||||
raw = parts[1] if len(parts) >= 2 else raw
|
||||
if raw.lower().startswith('json'):
|
||||
raw = raw[4:]
|
||||
raw = raw.strip()
|
||||
|
||||
# Extract first JSON object if extra text leaked through
|
||||
# Extract first JSON object if extra prose leaked through
|
||||
if raw and raw[0] == '{':
|
||||
brace_depth = 0
|
||||
end_idx = 0
|
||||
@@ -442,22 +448,18 @@ Respond with the JSON object only. No other text."""
|
||||
return json.loads(raw)
|
||||
except json.JSONDecodeError as exc:
|
||||
_logger.error("FL AI Engine: JSON parse error: %s\nRaw: %s", exc, raw[:500])
|
||||
raise RuntimeError(f"Ollama returned invalid JSON: {exc}") from exc
|
||||
raise RuntimeError(f"Claude returned invalid JSON: {exc}") from exc
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Step 7: Store analysis
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _store_analysis(self, analysis, result, matched_cases, complexity):
|
||||
"""
|
||||
Write parsed Ollama result into the fl.analysis record.
|
||||
"""
|
||||
# Normalize boolean field from Ollama (may come as string "true")
|
||||
"""Write parsed Claude result into the fl.analysis record."""
|
||||
attorney_flag = result.get('attorney_referral_flag', False)
|
||||
if isinstance(attorney_flag, str):
|
||||
attorney_flag = attorney_flag.lower() in ('true', '1', 'yes')
|
||||
|
||||
# Serialize list fields to JSON strings for Text fields
|
||||
petitioner_args = result.get('petitioner_arguments', [])
|
||||
respondent_counter = result.get('respondent_counterarguments', [])
|
||||
procedural_risks = result.get('procedural_risks', [])
|
||||
@@ -481,14 +483,13 @@ Respond with the JSON object only. No other text."""
|
||||
|
||||
analysis.write(vals)
|
||||
|
||||
# If attorney referral flagged, post urgent chatter message on the case
|
||||
if attorney_flag and analysis.case_id:
|
||||
analysis.case_id.message_post(
|
||||
body=(
|
||||
"<strong>⚠ AI ANALYSIS — ATTORNEY REFERRAL RECOMMENDED</strong><br/>"
|
||||
f"{result.get('attorney_referral_reason', 'Case complexity warrants legal counsel.')}<br/>"
|
||||
"FL Volunteer Lawyers Project: <a href='https://www.flvlp.org'>flvlp.org</a> | "
|
||||
"Three-Day Rule: 3-1-1 Legal Info: <a href='https://www.flcourts.gov'>flcourts.gov</a>"
|
||||
"3-1-1 Legal Info: <a href='https://www.flcourts.gov'>flcourts.gov</a>"
|
||||
),
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
|
||||
Reference in New Issue
Block a user