Align case stages to 5-stage spec and add Paralegal AI agent
Stage alignment: - Replace the 11 procedural stages with the spec's 5-stage machine (Intake/Active/Discovery/Pre-Trial/Closed); only Closed is folded - Make fl_stage_data.xml updatable (drop noupdate) so the rename applies on upgrade; keep fl_stage_intake/discovery/closed XML ids - Update case search filters to the new stage names (Intake/Active/Discovery/Pre-Trial) Issue-tag bug fix (fl_ai_engine): - Rule-based tagging and caselaw matching compared snake_case keys against the human-readable seeded tag names, so they never matched and issue_tag_ids was never populated. Add RULE_KEY_TO_TAG_NAME and translate keys to real names before all fl.issue.tag / fl.caselaw lookups Paralegal agent (fl.paralegal.agent, AbstractModel): - on_stage_change(): fast rule-based pass fired automatically on stage entry and case creation — generates the stage task batch (idempotent), recalculates filing/service deadlines, cross-references statutes by issue tag + case type, posts a chatter summary. No Claude call, so it never blocks the workflow - run_manual(): full pass adding a best-effort Claude procedural briefing with rule-based fallback; wired to a "Paralegal Review" button on the case form - AI audit-time logging is guarded behind a fl.timesheet existence check (model not built yet) - fl.case.write fires on_stage_change only when stage_id actually changes; create() generates the Intake batch Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,71 +1,40 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<odoo>
|
||||
<data noupdate="1">
|
||||
<data>
|
||||
|
||||
<record id="fl_stage_intake" model="fl.case.stage">
|
||||
<field name="name">Intake & Qualification</field>
|
||||
<field name="name">Intake</field>
|
||||
<field name="sequence">10</field>
|
||||
<field name="fold">False</field>
|
||||
<field name="description">Case created. Conflict check, questionnaire completion, fee waiver assessment.</field>
|
||||
</record>
|
||||
|
||||
<record id="fl_stage_preparation" model="fl.case.stage">
|
||||
<field name="name">Document Preparation</field>
|
||||
<record id="fl_stage_active" model="fl.case.stage">
|
||||
<field name="name">Active</field>
|
||||
<field name="sequence">20</field>
|
||||
<field name="fold">False</field>
|
||||
</record>
|
||||
|
||||
<record id="fl_stage_filed" model="fl.case.stage">
|
||||
<field name="name">Filed — Awaiting Service</field>
|
||||
<field name="sequence">30</field>
|
||||
<field name="fold">False</field>
|
||||
</record>
|
||||
|
||||
<record id="fl_stage_service_complete" model="fl.case.stage">
|
||||
<field name="name">Service Complete</field>
|
||||
<field name="sequence">40</field>
|
||||
<field name="fold">False</field>
|
||||
<field name="description">Intake complete and conflict check passed. Service of process, mandatory disclosure (FL-12.932), initial hearings.</field>
|
||||
</record>
|
||||
|
||||
<record id="fl_stage_discovery" model="fl.case.stage">
|
||||
<field name="name">Discovery</field>
|
||||
<field name="sequence">50</field>
|
||||
<field name="sequence">30</field>
|
||||
<field name="fold">False</field>
|
||||
<field name="description">Interrogatories, production requests, depositions per discovery suggestion wizard.</field>
|
||||
</record>
|
||||
|
||||
<record id="fl_stage_deposition" model="fl.case.stage">
|
||||
<field name="name">Deposition Stage</field>
|
||||
<field name="sequence">60</field>
|
||||
<field name="fold">False</field>
|
||||
</record>
|
||||
|
||||
<record id="fl_stage_mediation" model="fl.case.stage">
|
||||
<field name="name">Mediation</field>
|
||||
<field name="sequence">70</field>
|
||||
<field name="fold">False</field>
|
||||
</record>
|
||||
|
||||
<record id="fl_stage_hearing_scheduled" model="fl.case.stage">
|
||||
<field name="name">Hearing Scheduled</field>
|
||||
<field name="sequence">80</field>
|
||||
<field name="fold">False</field>
|
||||
</record>
|
||||
|
||||
<record id="fl_stage_order_entered" model="fl.case.stage">
|
||||
<field name="name">Order Entered</field>
|
||||
<field name="sequence">90</field>
|
||||
<record id="fl_stage_pretrial" model="fl.case.stage">
|
||||
<field name="name">Pre-Trial</field>
|
||||
<field name="sequence">40</field>
|
||||
<field name="fold">False</field>
|
||||
<field name="description">Discovery closed. Pretrial statement, exhibit list, witness list, mediation scheduling.</field>
|
||||
</record>
|
||||
|
||||
<record id="fl_stage_closed" model="fl.case.stage">
|
||||
<field name="name">Closed</field>
|
||||
<field name="sequence">100</field>
|
||||
<field name="fold">True</field>
|
||||
</record>
|
||||
|
||||
<record id="fl_stage_referred_out" model="fl.case.stage">
|
||||
<field name="name">Referred to Attorney</field>
|
||||
<field name="sequence">110</field>
|
||||
<field name="sequence">50</field>
|
||||
<field name="fold">True</field>
|
||||
<field name="description">Final order filed. Archive checklist, billing reconciliation, file retention notice.</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
|
||||
@@ -16,3 +16,4 @@ from . import fl_ai_engine
|
||||
from . import fl_argument
|
||||
from . import fl_case
|
||||
from . import fl_conflict_check
|
||||
from . import fl_paralegal_agent
|
||||
|
||||
@@ -25,19 +25,22 @@ CASE_FIELD_RULES = [
|
||||
]
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Caselaw topic → issue tag matching
|
||||
# Internal rule key → seeded fl.issue.tag display name (data/fl_issue_tags.xml).
|
||||
# Rule keys are the snake_case identifiers used in CASE_FIELD_RULES and the
|
||||
# tagging logic; the actual tag records use human-readable names, so all DB
|
||||
# lookups must translate through this map.
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
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')],
|
||||
RULE_KEY_TO_TAG_NAME = {
|
||||
'modification_threshold': 'Modification Threshold',
|
||||
'income_imputation': 'Income Imputation',
|
||||
'self_employment_income': 'Self-Employment Income',
|
||||
'timesharing_deviation': 'Timesharing Deviation',
|
||||
'domestic_violence': 'Domestic Violence',
|
||||
'fee_waiver': 'Fee Waiver / Indigent Status',
|
||||
'default_judgment': 'Default Judgment',
|
||||
'residency': 'Residency Requirement',
|
||||
'parenting_class': 'Parenting Class Required',
|
||||
'post_order': 'Post-Order / Income Withholding',
|
||||
}
|
||||
|
||||
MAX_CASELAW_IN_PROMPT = 8
|
||||
@@ -81,9 +84,13 @@ class FlAiEngine(models.AbstractModel):
|
||||
try:
|
||||
triggered_tags = self._rule_based_tagging(case)
|
||||
if triggered_tags:
|
||||
tag_names = [
|
||||
RULE_KEY_TO_TAG_NAME[k]
|
||||
for k in triggered_tags if k in RULE_KEY_TO_TAG_NAME
|
||||
]
|
||||
existing_tags = case.issue_tag_ids.mapped('name')
|
||||
new_tag_recs = self.env['fl.issue.tag'].search([
|
||||
('name', 'in', list(triggered_tags)),
|
||||
('name', 'in', tag_names),
|
||||
('name', 'not in', existing_tags),
|
||||
])
|
||||
if new_tag_recs:
|
||||
@@ -183,13 +190,14 @@ class FlAiEngine(models.AbstractModel):
|
||||
|
||||
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)
|
||||
tag_name = RULE_KEY_TO_TAG_NAME.get(tag)
|
||||
if not tag_name:
|
||||
continue
|
||||
recs = self.env['fl.caselaw'].search([
|
||||
('active', '=', True),
|
||||
('issue_tag_ids.name', '=', tag_name),
|
||||
], limit=6)
|
||||
matched_ids.update(recs.ids)
|
||||
|
||||
if not matched_ids:
|
||||
return self.env['fl.caselaw'].browse()
|
||||
|
||||
@@ -824,9 +824,12 @@ class FlCase(models.Model):
|
||||
# 4. Handle DV flag
|
||||
if record.domestic_violence_flag:
|
||||
record._handle_dv_flag()
|
||||
# 5. Paralegal agent — generate the Intake stage task batch
|
||||
self.env['fl.paralegal.agent'].on_stage_change(record)
|
||||
return records
|
||||
|
||||
def write(self, vals):
|
||||
stage_changed_recs = self.browse()
|
||||
# Block advancing past Intake until conflict check has passed
|
||||
if vals.get('stage_id'):
|
||||
new_stage = self.env['fl.case.stage'].browse(vals['stage_id'])
|
||||
@@ -841,7 +844,13 @@ class FlCase(models.Model):
|
||||
"conflict-of-interest check has not passed. Resolve or "
|
||||
"override the conflict before changing the stage."
|
||||
) % rec.name)
|
||||
stage_changed_recs = self.filtered(
|
||||
lambda r: r.stage_id.id != new_stage.id
|
||||
)
|
||||
result = super().write(vals)
|
||||
# Fire the paralegal agent (rule-based) when a case enters a new stage
|
||||
for rec in stage_changed_recs:
|
||||
self.env['fl.paralegal.agent'].on_stage_change(rec)
|
||||
# Recalculate service-anchored deadlines when service_date is set/changed
|
||||
if 'service_date' in vals:
|
||||
for rec in self:
|
||||
@@ -1136,3 +1145,17 @@ class FlCase(models.Model):
|
||||
'view_mode': 'form',
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
def action_run_paralegal(self):
|
||||
self.ensure_one()
|
||||
self.env['fl.paralegal.agent'].run_manual(self)
|
||||
return {
|
||||
'type': 'ir.actions.client',
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': _('Paralegal Agent'),
|
||||
'message': _('Procedural review complete — see the case chatter.'),
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
},
|
||||
}
|
||||
|
||||
323
activeblue_familylaw/models/fl_paralegal_agent.py
Normal file
323
activeblue_familylaw/models/fl_paralegal_agent.py
Normal file
@@ -0,0 +1,323 @@
|
||||
import json
|
||||
import logging
|
||||
|
||||
from odoo import _, models
|
||||
|
||||
from .fl_ai_engine import CLAUDE_MODEL
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
# Stage → procedural task batch (keyed by stage XML id, per the 5-stage machine
|
||||
# in CLAUDE.md). Each entry: (task name, description, sequence).
|
||||
# Task names are prefixed with the stage name at creation time, which also
|
||||
# provides idempotency (a batch already generated for a stage is not duplicated).
|
||||
# ─────────────────────────────────────────────────────────────────────────────
|
||||
STAGE_TASK_BATCHES = {
|
||||
'activeblue_familylaw.fl_stage_intake': [
|
||||
('Run conflict-of-interest check',
|
||||
'Screen all parties against other open cases before proceeding.', 10),
|
||||
('Complete client intake questionnaire',
|
||||
'Collect parties, children, income, and case facts.', 20),
|
||||
('Assess fee-waiver eligibility (FL 57.082)',
|
||||
'Check petitioner income vs 200% FPL; prepare FL-12.902(a) if eligible.', 30),
|
||||
],
|
||||
'activeblue_familylaw.fl_stage_active': [
|
||||
('Effect service of process',
|
||||
'Serve Summons + Petition; record the service date to start procedural clocks.', 10),
|
||||
('Serve mandatory disclosure (FL-12.932)',
|
||||
'Exchange FL 12.285 mandatory disclosure within 45 days of service.', 20),
|
||||
('Schedule initial hearings',
|
||||
'Calendar the case management conference and any temporary-relief hearings.', 30),
|
||||
],
|
||||
'activeblue_familylaw.fl_stage_discovery': [
|
||||
('Draft interrogatories',
|
||||
'Prepare written interrogatories directed to the opposing party.', 10),
|
||||
('Draft requests for production',
|
||||
'Request financial and supporting documents.', 20),
|
||||
('Schedule depositions',
|
||||
'Notice depositions where income or material facts are disputed.', 30),
|
||||
],
|
||||
'activeblue_familylaw.fl_stage_pretrial': [
|
||||
('Prepare pretrial statement',
|
||||
'Summarize stipulations, contested issues, and relief sought.', 10),
|
||||
('Compile exhibit list',
|
||||
'List and pre-mark all trial exhibits.', 20),
|
||||
('Compile witness list',
|
||||
'Identify and disclose all witnesses.', 30),
|
||||
('Schedule mediation',
|
||||
'Confirm court-ordered mediation; separate rooms if DV flagged (FL 44.102).', 40),
|
||||
],
|
||||
'activeblue_familylaw.fl_stage_closed': [
|
||||
('Complete archive checklist',
|
||||
'Confirm the final order is filed and all documents are stored.', 10),
|
||||
('Reconcile billing',
|
||||
'Finalize timesheet entries and issue the final invoice.', 20),
|
||||
('Send file retention notice',
|
||||
'Notify the client of the file retention / destruction policy.', 30),
|
||||
],
|
||||
}
|
||||
|
||||
# Issue tag display name (data/fl_issue_tags.xml) → fl.statute category.
|
||||
TAG_NAME_TO_STATUTE_CATEGORY = {
|
||||
'Modification Threshold': 'modification',
|
||||
'Income Imputation': 'child_support',
|
||||
'Self-Employment Income': 'child_support',
|
||||
'Timesharing Deviation': 'timesharing',
|
||||
'Domestic Violence': 'domestic_violence',
|
||||
'Fee Waiver / Indigent Status': 'fee_waiver',
|
||||
'Default Judgment': 'procedure',
|
||||
'Parenting Class Required': 'timesharing',
|
||||
'Residency Requirement': 'dissolution',
|
||||
'Post-Order / Income Withholding': 'enforcement',
|
||||
'Lifestyle Inconsistency': 'child_support',
|
||||
'Child Emancipation': 'modification',
|
||||
'Substantial Change in Circumstances': 'modification',
|
||||
'Alimony (2023 Reform)': 'alimony',
|
||||
}
|
||||
|
||||
# Base statute category implied by case type (always cross-referenced).
|
||||
CASE_TYPE_STATUTE_CATEGORY = {
|
||||
'modification': 'modification',
|
||||
'alimony_modification': 'alimony',
|
||||
'custody_modification': 'timesharing',
|
||||
'dissolution_children': 'dissolution',
|
||||
'dissolution_no_children': 'dissolution',
|
||||
'paternity': 'paternity',
|
||||
}
|
||||
|
||||
|
||||
class FlParalegalAgent(models.AbstractModel):
|
||||
"""
|
||||
Paralegal AI agent — procedural intelligence.
|
||||
|
||||
Two entry points:
|
||||
• on_stage_change(case) — fast rule-based pass fired automatically when a
|
||||
case enters a new stage (and on case creation). No Claude call, so it
|
||||
never adds latency or blocks the workflow.
|
||||
• run_manual(case) — full pass including a Claude-generated procedural
|
||||
briefing, fired on demand from the case form. Falls back to the
|
||||
rule-based summary when the API is unavailable.
|
||||
|
||||
Both passes generate the stage task batch, recalculate procedural deadlines,
|
||||
cross-reference statutes for the case's active issue tags, post a chatter
|
||||
summary, and log non-billable AI audit time (when fl.timesheet exists).
|
||||
"""
|
||||
_name = 'fl.paralegal.agent'
|
||||
_description = 'Paralegal AI Agent (procedural)'
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Public entry points
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def on_stage_change(self, case):
|
||||
"""Automatic rule-based pass for a stage entry. No Claude call."""
|
||||
tasks = self._generate_stage_tasks(case)
|
||||
self._recalculate_deadlines(case)
|
||||
statutes = self._cross_reference_statutes(case)
|
||||
self._post_summary(case, tasks, statutes, ai_narrative=None)
|
||||
self._log_ai_time(case, _('Paralegal stage pass: %s') % (
|
||||
case.stage_id.name or 'unknown'), ai_used=False)
|
||||
return tasks
|
||||
|
||||
def run_manual(self, case):
|
||||
"""Manual full pass including a best-effort Claude procedural briefing."""
|
||||
tasks = self._generate_stage_tasks(case)
|
||||
self._recalculate_deadlines(case)
|
||||
statutes = self._cross_reference_statutes(case)
|
||||
narrative = self._ai_procedural_summary(case, tasks, statutes)
|
||||
self._post_summary(case, tasks, statutes, ai_narrative=narrative)
|
||||
self._log_ai_time(case, _('Paralegal manual review (AI)'),
|
||||
ai_used=bool(narrative))
|
||||
return tasks
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Procedural building blocks (rule-based)
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _generate_stage_tasks(self, case):
|
||||
"""Create the current stage's task batch in the case project. Idempotent."""
|
||||
Task = self.env['project.task']
|
||||
if not case.stage_id or not case.project_id:
|
||||
return Task
|
||||
|
||||
batch = None
|
||||
for xmlid, tasks in STAGE_TASK_BATCHES.items():
|
||||
stage = self.env.ref(xmlid, raise_if_not_found=False)
|
||||
if stage and stage.id == case.stage_id.id:
|
||||
batch = tasks
|
||||
break
|
||||
if not batch:
|
||||
return Task
|
||||
|
||||
existing_names = set(Task.search([
|
||||
('project_id', '=', case.project_id.id)
|
||||
]).mapped('name'))
|
||||
|
||||
created = Task
|
||||
for name, description, sequence in batch:
|
||||
prefixed = f'[{case.stage_id.name}] {name}'
|
||||
if prefixed in existing_names:
|
||||
continue
|
||||
created |= Task.create({
|
||||
'name': prefixed,
|
||||
'description': description,
|
||||
'project_id': case.project_id.id,
|
||||
'sequence': sequence,
|
||||
})
|
||||
return created
|
||||
|
||||
def _recalculate_deadlines(self, case):
|
||||
"""Regenerate filing- and service-anchored deadlines (both idempotent)."""
|
||||
Deadline = self.env['fl.deadline']
|
||||
Deadline.generate_deadlines_for_case(case)
|
||||
Deadline.recalculate_service_deadlines(case)
|
||||
|
||||
def _cross_reference_statutes(self, case):
|
||||
"""Find FL statutes relevant to the case's issue tags and case type."""
|
||||
categories = set()
|
||||
for tag in case.issue_tag_ids:
|
||||
cat = TAG_NAME_TO_STATUTE_CATEGORY.get(tag.name)
|
||||
if cat:
|
||||
categories.add(cat)
|
||||
base = CASE_TYPE_STATUTE_CATEGORY.get(case.case_type)
|
||||
if base:
|
||||
categories.add(base)
|
||||
if not categories:
|
||||
return self.env['fl.statute']
|
||||
return self.env['fl.statute'].search([
|
||||
('active', '=', True),
|
||||
('category', 'in', list(categories)),
|
||||
])
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Summary / chatter
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _post_summary(self, case, tasks, statutes, ai_narrative=None):
|
||||
parts = ['<b>🧑⚖️ Paralegal Agent</b>',
|
||||
self._rule_based_summary(case, tasks, statutes)]
|
||||
if ai_narrative:
|
||||
parts.append(
|
||||
'<hr/><b>Procedural briefing:</b><br/>'
|
||||
+ ai_narrative.replace('\n', '<br/>')
|
||||
)
|
||||
case.message_post(
|
||||
body=''.join(f'<div>{p}</div>' for p in parts),
|
||||
subtype_xmlid='mail.mt_note',
|
||||
)
|
||||
|
||||
def _rule_based_summary(self, case, tasks, statutes):
|
||||
lines = [_('Processed stage: <b>%s</b>.') % (case.stage_id.name or 'unknown')]
|
||||
if tasks:
|
||||
items = ''.join(f'<li>{t.name}</li>' for t in tasks)
|
||||
lines.append(_('Generated %d task(s):') % len(tasks) + f'<ul>{items}</ul>')
|
||||
else:
|
||||
lines.append(_('No new tasks (batch already present).'))
|
||||
if statutes:
|
||||
lines.append(
|
||||
_('Relevant statutes: %s.') % ', '.join(statutes.mapped('name'))
|
||||
)
|
||||
open_dl = case.deadline_ids.filtered(
|
||||
lambda d: not d.completed and not d.waived
|
||||
)
|
||||
if open_dl:
|
||||
lines.append(_('%(n)d open deadline(s); next: %(next)s.') % {
|
||||
'n': len(open_dl),
|
||||
'next': case.next_deadline_label or '—',
|
||||
})
|
||||
return '<br/>'.join(lines)
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Claude procedural briefing (best-effort)
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _ai_procedural_summary(self, case, tasks, statutes):
|
||||
"""
|
||||
Generate a short procedural briefing via Claude. Returns plain text, or
|
||||
None on any failure (caller falls back to the rule-based summary).
|
||||
Never raises — AI must not block the workflow.
|
||||
"""
|
||||
try:
|
||||
import anthropic
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
api_key = self.env['ir.config_parameter'].sudo().get_param('fl_ai.claude_api_key')
|
||||
if not api_key:
|
||||
return None
|
||||
|
||||
complexity = (
|
||||
case.latest_analysis_id.case_complexity
|
||||
or self.env['fl.ai.engine']._fallback_complexity(case)
|
||||
)
|
||||
open_dl = case.deadline_ids.filtered(
|
||||
lambda d: not d.completed and not d.waived
|
||||
)
|
||||
context = {
|
||||
'case_type': case.case_type,
|
||||
'stage': case.stage_id.name if case.stage_id else 'unknown',
|
||||
'complexity': complexity,
|
||||
'issue_tags': case.issue_tag_ids.mapped('name'),
|
||||
'open_deadlines': [
|
||||
{'name': d.name, 'due_date': str(d.due_date),
|
||||
'statute': d.statute_reference or ''}
|
||||
for d in open_dl
|
||||
],
|
||||
'tasks_just_generated': tasks.mapped('name'),
|
||||
'relevant_statutes': statutes.mapped('name'),
|
||||
'domestic_violence': case.domestic_violence_flag,
|
||||
'respondent_has_counsel': case.respondent_has_counsel,
|
||||
}
|
||||
|
||||
system = (
|
||||
"You are a Florida family-law paralegal assistant for the 11th Judicial "
|
||||
"Circuit (Miami-Dade). You handle PROCEDURE, not legal advice. Given a "
|
||||
"case's current stage, open deadlines, generated tasks, and relevant "
|
||||
"statutes, write a concise procedural briefing for the legal team: what "
|
||||
"to do next, what deadlines are most urgent, and any procedural risks. "
|
||||
"Recommend attorney involvement when domestic violence, opposing counsel, "
|
||||
"or high complexity is present. Plain text only, under 200 words. Do not "
|
||||
"restate the input verbatim."
|
||||
)
|
||||
|
||||
try:
|
||||
client = anthropic.Anthropic(api_key=api_key)
|
||||
message = client.messages.create(
|
||||
model=CLAUDE_MODEL,
|
||||
max_tokens=700,
|
||||
system=system,
|
||||
messages=[{
|
||||
'role': 'user',
|
||||
'content': (
|
||||
'Case context:\n' + json.dumps(context, indent=2)
|
||||
+ '\n\nWrite the procedural briefing now.'
|
||||
),
|
||||
}],
|
||||
)
|
||||
return message.content[0].text.strip()
|
||||
except (anthropic.APIError, anthropic.APIConnectionError,
|
||||
anthropic.RateLimitError) as exc:
|
||||
_logger.warning("Paralegal agent AI briefing failed for case %s: %s",
|
||||
case.id, exc)
|
||||
return None
|
||||
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
# Audit time logging (guarded — fl.timesheet not yet built)
|
||||
# ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
def _log_ai_time(self, case, note, ai_used):
|
||||
"""Log a non-billable AI audit entry. No-op until fl.timesheet exists."""
|
||||
if 'fl.timesheet' not in self.env:
|
||||
return
|
||||
try:
|
||||
self.env['fl.timesheet'].create({
|
||||
'case_id': case.id,
|
||||
'name': note,
|
||||
'is_billable': False,
|
||||
'ai_agent': 'paralegal',
|
||||
'duration_hours': 0.05 if ai_used else 0.01,
|
||||
})
|
||||
except Exception as exc: # never block on audit logging
|
||||
_logger.warning("Paralegal AI-time logging skipped for case %s: %s",
|
||||
case.id, exc)
|
||||
@@ -21,6 +21,9 @@
|
||||
<button name="action_run_conflict_check" string="Run Conflict Check"
|
||||
type="object"
|
||||
groups="activeblue_familylaw.group_admin,activeblue_familylaw.group_paralegal"/>
|
||||
<button name="action_run_paralegal" string="Paralegal Review"
|
||||
type="object"
|
||||
groups="activeblue_familylaw.group_admin,activeblue_familylaw.group_paralegal"/>
|
||||
</header>
|
||||
|
||||
<!-- Attorney Referral Banner -->
|
||||
@@ -350,11 +353,13 @@
|
||||
<filter string="Active Cases" name="active"
|
||||
domain="[('active', '=', True)]"/>
|
||||
<filter string="Intake" name="stage_intake"
|
||||
domain="[('stage_id.name', '=', 'Intake & Qualification')]"/>
|
||||
<filter string="Filed" name="stage_filed"
|
||||
domain="[('stage_id.name', '=', 'Filed — Awaiting Service')]"/>
|
||||
domain="[('stage_id.name', '=', 'Intake')]"/>
|
||||
<filter string="Active" name="stage_active"
|
||||
domain="[('stage_id.name', '=', 'Active')]"/>
|
||||
<filter string="Discovery" name="stage_discovery"
|
||||
domain="[('stage_id.name', '=', 'Discovery')]"/>
|
||||
<filter string="Pre-Trial" name="stage_pretrial"
|
||||
domain="[('stage_id.name', '=', 'Pre-Trial')]"/>
|
||||
<separator/>
|
||||
<filter string="Modification Cases" name="type_modification"
|
||||
domain="[('case_type', '=', 'modification')]"/>
|
||||
|
||||
Reference in New Issue
Block a user