Phase 5: Full AI engine + caselaw seed data (23 FL cases)
fl_ai_engine.py: Complete Ollama integration with 7-step pipeline: rule-based issue tagging, caselaw matching (3rd DCA-prioritized), case context serialization, prompt construction, Ollama HTTP call (llama3.1 at 192.168.2.10:11434), JSON parsing with fence-strip, and fl.analysis persistence with attorney referral chatter alert. fl_caselaw_data.xml: 23 seeded Florida cases covering modification threshold (Daly, Regan, Pimm, El Kohen, Rolfe), income imputation (Barner, Sitterson), self-employment (Smith, Young), timesharing (Freid, Kennedy, Boykin), domestic violence (Conlin, Kahle), default judgment (Lindsey, North), residency (Fults), parenting class (Maddox), fee waiver (Abdool, Kielbania), disclosure, withholding, above-schedule, and discovery sanctions. fl_case.py: trigger_ai_analysis() wired to engine.analyze_case(); returns form popup of fl.analysis result. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,51 +1,389 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from odoo import models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
OLLAMA_URL = 'http://192.168.2.10:11434/api/generate'
|
||||
OLLAMA_MODEL = 'llama3.1'
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Rule-based issue tag weights
|
||||
# Maps (field_name, value_or_True) → list of issue tag XML ids
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
ISSUE_RULES = [
|
||||
# Modification threshold
|
||||
('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
|
||||
('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')],
|
||||
'income_imputation': [('issue_tag_ids.name', '=', 'income_imputation')],
|
||||
'self_employment_income': [('issue_tag_ids.name', '=', 'self_employment_income')],
|
||||
'timesharing_deviation': [('issue_tag_ids.name', '=', 'timesharing_deviation')],
|
||||
'domestic_violence': [('issue_tag_ids.name', '=', 'domestic_violence')],
|
||||
'fee_waiver': [('issue_tag_ids.name', '=', 'fee_waiver')],
|
||||
'default_judgment': [('issue_tag_ids.name', '=', 'default_judgment')],
|
||||
'residency': [('issue_tag_ids.name', '=', 'residency')],
|
||||
'parenting_class': [('issue_tag_ids.name', '=', 'parenting_class')],
|
||||
'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.
|
||||
Phase 1: Stub service model.
|
||||
Phase 5 — Full Ollama integration with rule-based pre-processing.
|
||||
|
||||
This is an AbstractModel — not stored in the database.
|
||||
Used as a service class for AI analysis calls.
|
||||
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
|
||||
6. _store_analysis — persist fl.analysis record with results
|
||||
|
||||
AbstractModel — not stored in the database.
|
||||
"""
|
||||
_name = 'fl.ai.engine'
|
||||
_description = 'Family Law AI Analysis Engine (Ollama)'
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Public entry point
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def analyze_case(self, case_id):
|
||||
"""
|
||||
Phase 5 entry point.
|
||||
Full workflow:
|
||||
1. Rule-based issue tagging
|
||||
2. Build case context JSON
|
||||
3. Call Ollama (llama3.1)
|
||||
4. Parse JSON response
|
||||
5. Store fl.analysis record
|
||||
Full Phase 5 analysis entry point.
|
||||
Returns fl.analysis record.
|
||||
"""
|
||||
case = self.env['fl.case'].browse(case_id)
|
||||
analysis = self.env['fl.analysis'].create({
|
||||
if not case.exists():
|
||||
raise ValueError(f"Case {case_id} not found")
|
||||
|
||||
analysis_vals = {
|
||||
'case_id': case.id,
|
||||
'state': 'pending',
|
||||
'model_used': OLLAMA_MODEL,
|
||||
'plain_english_summary': (
|
||||
'AI analysis not yet implemented. '
|
||||
'Full analysis will be available in Phase 5.'
|
||||
),
|
||||
'plain_english_summary_es': (
|
||||
'El análisis de IA aún no está implementado. '
|
||||
'El análisis completo estará disponible en la Fase 5.'
|
||||
),
|
||||
'state': 'complete',
|
||||
})
|
||||
}
|
||||
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', '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
|
||||
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)
|
||||
analysis.write({
|
||||
'state': 'failed',
|
||||
'error_message': str(exc),
|
||||
'plain_english_summary': (
|
||||
"AI analysis could not be completed at this time. "
|
||||
"Please try again later or contact support."
|
||||
),
|
||||
'plain_english_summary_es': (
|
||||
"El análisis de IA no pudo completarse en este momento. "
|
||||
"Por favor intente más tarde o contacte soporte."
|
||||
),
|
||||
})
|
||||
|
||||
return analysis
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Step 1: Rule-based tagging
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _rule_based_tagging(self, case):
|
||||
"""
|
||||
Apply deterministic rules to identify legal issues present 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
|
||||
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)
|
||||
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
|
||||
|
||||
return triggered
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Step 2: Match caselaw
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
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.
|
||||
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, [])
|
||||
if domain:
|
||||
recs = self.env['fl.caselaw'].search(
|
||||
[('active', '=', True)] + domain,
|
||||
limit=6,
|
||||
)
|
||||
matched_ids.update(recs.ids)
|
||||
|
||||
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,
|
||||
}
|
||||
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]])
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Step 3: Build case context
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
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.
|
||||
"""
|
||||
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,
|
||||
},
|
||||
'respondent': {
|
||||
'employment': case.respondent_id.employment_status if case.respondent_id else 'unknown',
|
||||
'monthly_net_income': case.respondent_net_income or 0,
|
||||
'answered': case.respondent_answered,
|
||||
'has_counsel': case.respondent_has_counsel,
|
||||
},
|
||||
'support': {
|
||||
'current_order_amount': case.current_order_amount 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,
|
||||
},
|
||||
'children': [
|
||||
{
|
||||
'age': c.age,
|
||||
'approaching_emancipation': c.approaching_emancipation,
|
||||
'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,
|
||||
},
|
||||
'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,
|
||||
'issue_tags': list(case.issue_tag_ids.mapped('name')),
|
||||
'caselaw': [
|
||||
{
|
||||
'citation': cl.citation,
|
||||
'holding': (cl.holding or '')[:300],
|
||||
'favorable_to': cl.favorable_to,
|
||||
}
|
||||
for cl in matched_cases
|
||||
],
|
||||
}
|
||||
return context
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Step 4: Complexity assessment
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _assess_complexity(self, case, triggered_tags):
|
||||
"""
|
||||
Simple heuristic complexity scorer.
|
||||
Returns 'simple', 'moderate', or 'complex'.
|
||||
"""
|
||||
score = 0
|
||||
if case.domestic_violence_flag:
|
||||
score += 3
|
||||
if case.respondent_has_counsel:
|
||||
score += 2
|
||||
if 'income_imputation' in triggered_tags:
|
||||
score += 2
|
||||
if 'self_employment_income' in triggered_tags:
|
||||
score += 2
|
||||
if case.child_ids and len(case.child_ids) > 2:
|
||||
score += 1
|
||||
if triggered_tags:
|
||||
score += len(triggered_tags)
|
||||
|
||||
if score <= 3:
|
||||
return 'simple'
|
||||
elif score <= 7:
|
||||
return 'moderate'
|
||||
else:
|
||||
return 'complex'
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Step 5: Build Ollama 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.
|
||||
"""
|
||||
context_json = json.dumps(context, indent=2)
|
||||
|
||||
attorney_trigger = (
|
||||
context.get('domestic_violence') or
|
||||
context.get('respondent', {}).get('has_counsel') or
|
||||
complexity == 'complex'
|
||||
)
|
||||
|
||||
prompt = f"""You are an AI legal assistant for a Florida family law case management system. \
|
||||
Your role is to help pro se (self-represented) litigants in Miami-Dade County understand their case. \
|
||||
You are NOT providing legal advice — you are explaining the legal framework and procedure.
|
||||
|
||||
IMPORTANT RULES:
|
||||
- Always recommend an attorney when the case involves domestic violence, opposing counsel, or high complexity.
|
||||
- Florida uses the FL 61.30 income-shares model for child support.
|
||||
- Modification threshold: 15% AND $50 difference required (FL 61.30(1)(b)).
|
||||
- Timesharing credit applies when either parent has >73 overnights/year (FL 61.30(11)(b)).
|
||||
- All output must be JSON only — no markdown, no prose outside the JSON object.
|
||||
|
||||
CASE DATA:
|
||||
{context_json}
|
||||
|
||||
TASK: Analyze this case and return a JSON object with exactly these fields:
|
||||
{{
|
||||
"plain_english_summary": "3-5 sentence plain English explanation of the case situation and key legal issues — no jargon",
|
||||
"plain_english_summary_es": "Same 3-5 sentences in Spanish",
|
||||
"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_reason": "reason if flag is true, else null",
|
||||
"confidence_level": "high|medium|low",
|
||||
"case_complexity": "{complexity}",
|
||||
"next_steps": ["step 1", "step 2", "step 3"]
|
||||
}}
|
||||
|
||||
Respond with the JSON object only. No other text."""
|
||||
|
||||
return prompt
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# Step 6: Call Ollama
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _call_ollama(self, prompt):
|
||||
"""Call Ollama API and return parsed JSON response."""
|
||||
"""
|
||||
Call Ollama API (llama3.1) and return parsed JSON dict.
|
||||
Raises on network error or JSON parse failure.
|
||||
"""
|
||||
try:
|
||||
import requests
|
||||
except ImportError:
|
||||
@@ -53,6 +391,9 @@ class FlAiEngine(models.AbstractModel):
|
||||
'requests library not available. '
|
||||
'Install with: pip install requests'
|
||||
)
|
||||
|
||||
_logger.info("FL AI Engine: calling Ollama at %s (model=%s)", OLLAMA_URL, OLLAMA_MODEL)
|
||||
|
||||
response = requests.post(
|
||||
OLLAMA_URL,
|
||||
json={
|
||||
@@ -68,11 +409,92 @@ class FlAiEngine(models.AbstractModel):
|
||||
timeout=180,
|
||||
)
|
||||
response.raise_for_status()
|
||||
|
||||
raw = response.json().get('response', '{}').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('```')
|
||||
raw = parts[1] if len(parts) > 1 else raw
|
||||
if raw.startswith('json'):
|
||||
if len(parts) >= 3:
|
||||
raw = parts[1]
|
||||
elif len(parts) == 2:
|
||||
raw = parts[1]
|
||||
if raw.lower().startswith('json'):
|
||||
raw = raw[4:]
|
||||
return json.loads(raw.strip())
|
||||
raw = raw.strip()
|
||||
|
||||
# Extract first JSON object if extra text leaked through
|
||||
if raw and raw[0] == '{':
|
||||
brace_depth = 0
|
||||
end_idx = 0
|
||||
for i, ch in enumerate(raw):
|
||||
if ch == '{':
|
||||
brace_depth += 1
|
||||
elif ch == '}':
|
||||
brace_depth -= 1
|
||||
if brace_depth == 0:
|
||||
end_idx = i + 1
|
||||
break
|
||||
raw = raw[:end_idx]
|
||||
|
||||
try:
|
||||
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
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────────
|
||||
# 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")
|
||||
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', [])
|
||||
|
||||
vals = {
|
||||
'state': 'complete',
|
||||
'plain_english_summary': result.get('plain_english_summary', ''),
|
||||
'plain_english_summary_es': result.get('plain_english_summary_es', ''),
|
||||
'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', complexity),
|
||||
'petitioner_arguments': json.dumps(petitioner_args, ensure_ascii=False, indent=2),
|
||||
'respondent_counterarguments': json.dumps(respondent_counter, ensure_ascii=False, indent=2),
|
||||
'procedural_risks': json.dumps(procedural_risks, ensure_ascii=False, indent=2),
|
||||
'raw_response': json.dumps(result, ensure_ascii=False, indent=2),
|
||||
}
|
||||
|
||||
if matched_cases:
|
||||
vals['matched_caselaw_ids'] = [(6, 0, matched_cases.ids)]
|
||||
|
||||
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>"
|
||||
),
|
||||
message_type='comment',
|
||||
subtype_xmlid='mail.mt_comment',
|
||||
)
|
||||
|
||||
_logger.info(
|
||||
"FL AI Engine: analysis %s complete (complexity=%s, attorney_referral=%s)",
|
||||
analysis.id, complexity, attorney_flag
|
||||
)
|
||||
|
||||
@@ -888,13 +888,24 @@ class FlCase(models.Model):
|
||||
|
||||
def trigger_ai_analysis(self):
|
||||
"""
|
||||
Trigger AI case analysis via Ollama.
|
||||
Stub for Phase 1 — full implementation in Phase 5.
|
||||
Trigger AI case analysis via Ollama (fl.ai.engine).
|
||||
Phase 5 — full implementation.
|
||||
"""
|
||||
self.ensure_one()
|
||||
engine = self.env['fl.ai.engine']
|
||||
self.message_post(
|
||||
body='🤖 AI analysis queued. Full AI analysis will be available in Phase 5.',
|
||||
body='🤖 AI analysis started. This may take up to 3 minutes...',
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
analysis = engine.analyze_case(self.id)
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': 'AI Analysis Result',
|
||||
'res_model': 'fl.analysis',
|
||||
'res_id': analysis.id,
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
# ══════════════════════════════════════════════════════════════════════
|
||||
# ACTIONS
|
||||
|
||||
Reference in New Issue
Block a user