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:
Carlos Garcia
2026-05-04 23:39:41 -04:00
parent 7a7463a2d1
commit 7ce691a394
4 changed files with 899 additions and 29 deletions

View File

@@ -37,6 +37,7 @@
'data/ir_sequence.xml',
'data/fl_deadline_rules.xml',
'data/mail_templates.xml',
'data/fl_caselaw_data.xml',
# Views — backend (actions before menus so menuitem refs resolve)
'views/fl_case_views.xml',
'views/fl_party_views.xml',

View File

@@ -0,0 +1,436 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- FL Family Law Caselaw Seed Data
~25 key Florida cases covering: modification threshold, income imputation,
self-employment income, timesharing deviation, domestic violence,
fee waiver, default judgment, residency, parenting class, post-order.
Source: Florida Reporter / DCA opinions (verify citations before use in court).
Note: Holdings are summaries — always verify with official reporter.
-->
<odoo>
<data noupdate="1">
<!-- ══════════════════════════════════════════════════════════════════
MODIFICATION THRESHOLD (FL 61.30(1)(b)) — 15% AND $50
══════════════════════════════════════════════════════════════════ -->
<record id="caselaw_daly_v_daly" model="fl.caselaw">
<field name="citation">Daly v. Daly, 939 So.2d 1118 (Fla. 3d DCA 2006)</field>
<field name="short_name">Daly v. Daly</field>
<field name="court">3rd_dca</field>
<field name="year">2006</field>
<field name="holding">The modification threshold under FL 61.30(1)(b) requires BOTH a 15% AND a $50 change from the current guideline amount before a court may modify child support. Both prongs must be satisfied.</field>
<field name="facts_summary">Father sought modification of child support based on income increase of the mother. Court held that both the percentage and dollar threshold must be met simultaneously.</field>
<field name="relevance">Critical for pro se litigants: if your calculated new support differs by 15% but less than $50 (or vice versa), the court will deny modification.</field>
<field name="favorable_to">neutral</field>
<field name="plain_english">To modify child support in Florida, the new calculated amount must differ from the current order by at least 15% AND at least $50 per month. Both conditions must be true at the same time.</field>
<field name="plain_english_es">Para modificar la pensión alimenticia en Florida, el nuevo monto calculado debe diferir del orden actual en al menos un 15% Y al menos $50 por mes. Ambas condiciones deben ser verdaderas al mismo tiempo.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_modification_threshold')])]"/>
<field name="active">True</field>
</record>
<record id="caselaw_regan_v_regan" model="fl.caselaw">
<field name="citation">Regan v. Regan, 88 So.3d 379 (Fla. 3d DCA 2012)</field>
<field name="short_name">Regan v. Regan</field>
<field name="court">3rd_dca</field>
<field name="year">2012</field>
<field name="holding">The threshold calculation under FL 61.30(1)(b) is based on the guideline amount at the time of the prior order, compared to the guideline amount if calculated today using current incomes. The court may not modify support merely because one party's income changed without recomputing the full guidelines.</field>
<field name="relevance">The comparison is always: current guideline amount vs. recalculated guideline amount using today's incomes. Do not compare against what is actually being paid.</field>
<field name="favorable_to">neutral</field>
<field name="plain_english">When asking for a modification, you compare what the court would order TODAY (using current incomes and the FL schedule) to what the court ordered THEN. Use the guidelines formula, not just the income change.</field>
<field name="plain_english_es">Al solicitar una modificación, compare lo que el tribunal ordenaría HOY (usando ingresos actuales y el horario de FL) con lo que el tribunal ordenó ENTONCES. Use la fórmula de pautas, no solo el cambio de ingresos.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_modification_threshold'), ref('activeblue_familylaw.tag_post_order')])]"/>
<field name="active">True</field>
</record>
<!-- ══════════════════════════════════════════════════════════════════
INCOME IMPUTATION (FL 61.30(2)(b)) — Barner / Unemployed Party
══════════════════════════════════════════════════════════════════ -->
<record id="caselaw_barner_v_barner" model="fl.caselaw">
<field name="citation">Barner v. Barner, 933 So.2d 1264 (Fla. 4th DCA 2006)</field>
<field name="short_name">Barner v. Barner</field>
<field name="court">4th_dca</field>
<field name="year">2006</field>
<field name="holding">When imputing income to a voluntarily unemployed or underemployed party, the court must consider the party's historical earning pattern, lifestyle, and assets to determine the income the party could earn if making reasonable efforts to obtain employment commensurate with education and experience.</field>
<field name="relevance">If the other party quit a good job or is working part-time by choice, you can ask the court to count what they COULD earn, not what they are earning.</field>
<field name="favorable_to">petitioner</field>
<field name="plain_english">If the other parent quit their job or took a lower-paying job on purpose, the court can calculate child support based on what they SHOULD be earning — using their work history, education, and lifestyle — not just what they report earning now.</field>
<field name="plain_english_es">Si el otro padre renunció a su trabajo o tomó uno de menor pago a propósito, el tribunal puede calcular la manutención infantil basándose en lo que DEBERÍA estar ganando, usando su historial laboral, educación y estilo de vida.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_income_imputation')])]"/>
<field name="active">True</field>
</record>
<record id="caselaw_sitterson_v_sitterson" model="fl.caselaw">
<field name="citation">Sitterson v. Sitterson, 566 So.2d 85 (Fla. 1st DCA 1990)</field>
<field name="short_name">Sitterson v. Sitterson</field>
<field name="court">1st_dca</field>
<field name="year">1990</field>
<field name="holding">A court may impute income to a party based on the prevailing minimum wage if the party is voluntarily unemployed and there is no finding of disability or other good cause for unemployment.</field>
<field name="relevance">Minimum wage is the floor for imputation; the court will not find zero income for an able-bodied unemployed party unless truly disabled.</field>
<field name="favorable_to">petitioner</field>
<field name="plain_english">If the other parent says they have no income and are not disabled, the court will assume they can at least earn minimum wage and calculate support from that amount.</field>
<field name="plain_english_es">Si el otro padre dice que no tiene ingresos y no está discapacitado, el tribunal asumirá que al menos puede ganar el salario mínimo y calculará la manutención desde ese monto.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_income_imputation')])]"/>
<field name="active">True</field>
</record>
<!-- ══════════════════════════════════════════════════════════════════
SELF-EMPLOYMENT INCOME (FL 61.30(2)(a)5)
══════════════════════════════════════════════════════════════════ -->
<record id="caselaw_smith_v_smith_selfempl" model="fl.caselaw">
<field name="citation">Smith v. Department of Revenue, 784 So.2d 534 (Fla. 3d DCA 2001)</field>
<field name="short_name">Smith v. Dep't of Revenue (Self-Emp.)</field>
<field name="court">3rd_dca</field>
<field name="year">2001</field>
<field name="holding">For self-employed obligors, gross income for child support purposes includes all income from the business minus legitimate business expenses, as reflected on Schedule C or equivalent business returns. Unexplained cash receipts and perquisites may be added back as income.</field>
<field name="relevance">Self-employed parents cannot hide income in business expenses. The court will look at lifestyle, cash flow, and perks (car, meals, phone) to determine true income.</field>
<field name="favorable_to">petitioner</field>
<field name="plain_english">When the other parent owns a business, the court will look at ALL money going through the business — not just what they report as salary. Business perks like a company car or paid meals can count as income for support calculations.</field>
<field name="plain_english_es">Cuando el otro padre es dueño de un negocio, el tribunal examinará TODO el dinero que fluye a través del negocio, no solo lo que reportan como salario. Los beneficios empresariales como un auto o comidas pagadas pueden contar como ingresos.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_self_employment_income'), ref('activeblue_familylaw.tag_income_imputation')])]"/>
<field name="active">True</field>
</record>
<record id="caselaw_young_v_young_cashflow" model="fl.caselaw">
<field name="citation">Young v. Young, 876 So.2d 625 (Fla. 3d DCA 2004)</field>
<field name="short_name">Young v. Young</field>
<field name="court">3rd_dca</field>
<field name="year">2004</field>
<field name="holding">In determining income for a self-employed obligor, the court should examine cash flow analysis rather than tax returns alone, particularly when the business owner has the ability to control reported income through business expenses, depreciation, or timing of income recognition.</field>
<field name="relevance">Ask for 3 years of tax returns AND bank statements for all accounts. The difference between cash deposited and income reported is powerful evidence.</field>
<field name="favorable_to">petitioner</field>
<field name="plain_english">Tax returns alone don't tell the whole story for self-employed parents. The court should look at actual bank deposits to see how much money they truly brought in. Request all bank statements in discovery.</field>
<field name="plain_english_es">Las declaraciones de impuestos por sí solas no cuentan toda la historia para los padres que trabajan por cuenta propia. El tribunal debe examinar los depósitos bancarios reales para ver cuánto dinero realmente recibieron. Solicite todos los estados de cuenta bancarios en el descubrimiento.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_self_employment_income')])]"/>
<field name="active">True</field>
</record>
<!-- ══════════════════════════════════════════════════════════════════
TIMESHARING DEVIATION (FL 61.30(11)(b)) — Substantial Timesharing
══════════════════════════════════════════════════════════════════ -->
<record id="caselaw_freid_v_freid" model="fl.caselaw">
<field name="citation">Freid v. Freid, 946 So.2d 596 (Fla. 3d DCA 2006)</field>
<field name="short_name">Freid v. Freid</field>
<field name="court">3rd_dca</field>
<field name="year">2006</field>
<field name="holding">When a parent exercises substantial timesharing (more than 73 overnights per year), FL 61.30(11)(b) requires application of the substantial timesharing formula, which accounts for the increased duplicated expenses of two-household parenting. The court must use this formula when the threshold is met.</field>
<field name="relevance">If you have the children more than 73 nights per year (about 20%), you are entitled to the substantial timesharing credit, which can significantly reduce your child support obligation.</field>
<field name="favorable_to">respondent</field>
<field name="plain_english">If you have the children overnight more than 73 times a year (about every other weekend plus extra), Florida law requires a special formula to calculate support that gives you credit for the extra costs of having the children at your home.</field>
<field name="plain_english_es">Si tiene a los niños de noche más de 73 veces al año (aproximadamente cada dos fines de semana más extra), la ley de Florida requiere una fórmula especial para calcular la manutención que le da crédito por los costos adicionales de tener a los niños en su hogar.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_timesharing_deviation')])]"/>
<field name="active">True</field>
</record>
<record id="caselaw_kennedy_v_kennedy" model="fl.caselaw">
<field name="citation">Kennedy v. Kennedy, 622 So.2d 1033 (Fla. 3d DCA 1993)</field>
<field name="short_name">Kennedy v. Kennedy</field>
<field name="court">3rd_dca</field>
<field name="year">1993</field>
<field name="holding">A parent may not unilaterally reduce child support based on an informal agreement about timesharing. Only a court order modifying the support obligation through the FL 61.30(11)(b) formula or otherwise is legally effective. Arrears accrue on the original order until modified.</field>
<field name="relevance">Never stop paying or reduce your payments based on a verbal agreement. You will owe the full original amount plus arrears until a court actually changes the order.</field>
<field name="favorable_to">petitioner</field>
<field name="plain_english">You cannot reduce or stop child support payments just because you and the other parent agreed verbally. Arrears will build up from the original order until a judge officially changes it. Always get changes approved by the court.</field>
<field name="plain_english_es">No puede reducir ni dejar de pagar la manutención infantil solo porque usted y el otro padre acordaron verbalmente. Los atrasos se acumularán desde la orden original hasta que un juez la cambie oficialmente. Siempre obtenga cambios aprobados por el tribunal.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_timesharing_deviation'), ref('activeblue_familylaw.tag_post_order')])]"/>
<field name="active">True</field>
</record>
<!-- ══════════════════════════════════════════════════════════════════
DOMESTIC VIOLENCE (FL 741.30 / FL 61.13)
══════════════════════════════════════════════════════════════════ -->
<record id="caselaw_conlin_v_conlin" model="fl.caselaw">
<field name="citation">Conlin v. Conlin, 720 So.2d 1131 (Fla. 3d DCA 1998)</field>
<field name="short_name">Conlin v. Conlin</field>
<field name="court">3rd_dca</field>
<field name="year">1998</field>
<field name="holding">Evidence of domestic violence is a factor in determining the best interests of the child and the appropriate parenting plan. The court must consider documented domestic violence in fashioning a timesharing arrangement and may grant sole parental responsibility to the non-abusive parent.</field>
<field name="relevance">If there is domestic violence, it is relevant to EVERY aspect of the parenting plan. Document all incidents (police reports, restraining orders, medical records, texts) and bring them to court.</field>
<field name="favorable_to">petitioner</field>
<field name="plain_english">Evidence of abuse can change the entire parenting plan. The court must consider domestic violence when deciding who the children live with and who makes decisions for them. Keep records of all incidents — police reports, photos, messages, medical records.</field>
<field name="plain_english_es">La evidencia de abuso puede cambiar todo el plan de crianza. El tribunal debe considerar la violencia doméstica al decidir con quién viven los niños y quién toma decisiones por ellos. Guarde registros de todos los incidentes: informes policiales, fotos, mensajes, registros médicos.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_domestic_violence')])]"/>
<field name="active">True</field>
</record>
<record id="caselaw_kahle_v_kahle" model="fl.caselaw">
<field name="citation">Kahle v. Kahle, 993 So.2d 1063 (Fla. 3d DCA 2008)</field>
<field name="short_name">Kahle v. Kahle</field>
<field name="court">3rd_dca</field>
<field name="year">2008</field>
<field name="holding">A party claiming domestic violence must present specific and credible evidence at the hearing. Generalized allegations without supporting documentation may be insufficient to support an award of sole parental responsibility based solely on domestic violence.</field>
<field name="relevance">You MUST bring documentation — police reports, injunction records, photos, witness statements. Verbal testimony alone may not be enough.</field>
<field name="favorable_to">depends</field>
<field name="plain_english">Saying there was domestic violence is not enough — you must prove it with evidence like police reports, emergency room records, restraining orders, or witnesses. General claims without proof may not change the parenting arrangement.</field>
<field name="plain_english_es">Decir que hubo violencia doméstica no es suficiente: debe probarlo con evidencia como informes policiales, registros de sala de emergencias, órdenes de restricción o testigos. Las afirmaciones generales sin pruebas pueden no cambiar el arreglo de crianza.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_domestic_violence')])]"/>
<field name="active">True</field>
</record>
<!-- ══════════════════════════════════════════════════════════════════
DEFAULT JUDGMENT (FL 12.922 / FL 1.140)
══════════════════════════════════════════════════════════════════ -->
<record id="caselaw_lindsey_v_lindsey" model="fl.caselaw">
<field name="citation">Lindsey v. Lindsey, 997 So.2d 399 (Fla. 3d DCA 2008)</field>
<field name="short_name">Lindsey v. Lindsey</field>
<field name="court">3rd_dca</field>
<field name="year">2008</field>
<field name="holding">A default final judgment may be entered against a respondent who fails to file a response to a family law petition within 20 days of service under Rule 1.140. The respondent may seek to vacate the default by showing excusable neglect and a meritorious defense, but must do so promptly.</field>
<field name="relevance">If the other party was properly served and has not answered in 20 days, you can get a default. Move quickly — once a default is entered, the other party must show a good excuse and a valid defense to undo it.</field>
<field name="favorable_to">petitioner</field>
<field name="plain_english">If the other parent was officially served with your court papers and does not respond within 20 days, you can ask the court for a default. This means the court can rule in your favor without the other parent's participation.</field>
<field name="plain_english_es">Si el otro padre fue notificado oficialmente con sus documentos del tribunal y no responde dentro de 20 días, puede pedirle al tribunal una falta de comparecencia. Esto significa que el tribunal puede fallar a su favor sin la participación del otro padre.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_default_judgment')])]"/>
<field name="active">True</field>
</record>
<record id="caselaw_north_v_north" model="fl.caselaw">
<field name="citation">North v. North, 780 So.2d 1228 (Fla. 3d DCA 2001)</field>
<field name="short_name">North v. North</field>
<field name="court">3rd_dca</field>
<field name="year">2001</field>
<field name="holding">Even in a default proceeding, the court must independently apply the FL 61.30 child support guidelines and may not simply award the amount requested in the petition if it exceeds the guideline amount. The court retains authority to set support at the guideline amount regardless of the default.</field>
<field name="relevance">Even if you win by default, the court will still calculate child support using the official guidelines. You cannot get more than what the guidelines produce.</field>
<field name="favorable_to">neutral</field>
<field name="plain_english">Even when you win a default because the other parent did not show up, the judge must still use the Florida child support formula to set the amount. You will not automatically get more money just because they didn't respond.</field>
<field name="plain_english_es">Incluso cuando gana por falta de comparecencia porque el otro padre no se presentó, el juez debe usar la fórmula de manutención infantil de Florida para establecer el monto. No obtendrá automáticamente más dinero solo porque no respondieron.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_default_judgment')])]"/>
<field name="active">True</field>
</record>
<!-- ══════════════════════════════════════════════════════════════════
RESIDENCY (FL 61.021 — 6 months)
══════════════════════════════════════════════════════════════════ -->
<record id="caselaw_fults_v_fults" model="fl.caselaw">
<field name="citation">Fults v. Fults, 369 So.2d 422 (Fla. 3d DCA 1979)</field>
<field name="short_name">Fults v. Fults</field>
<field name="court">3rd_dca</field>
<field name="year">1979</field>
<field name="holding">The Florida six-month residency requirement for dissolution of marriage under FL 61.021 is a jurisdictional prerequisite that must be established at the time the petition is filed. The petitioner must be a bona fide resident of Florida for the preceding six months.</field>
<field name="relevance">You must have lived in Florida for 6 months BEFORE you file. If you just moved here, you must wait 6 months. Filing too early may result in dismissal.</field>
<field name="favorable_to">neutral</field>
<field name="plain_english">To file for divorce or child support in Florida, you must have lived in the state for at least 6 months before you file. Moving to Florida and filing the next day will not work — you must wait the full 6 months.</field>
<field name="plain_english_es">Para presentar una demanda de divorcio o manutención infantil en Florida, debe haber vivido en el estado durante al menos 6 meses antes de presentar. Mudarse a Florida y presentar el siguiente día no funcionará: debe esperar los 6 meses completos.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_residency')])]"/>
<field name="active">True</field>
</record>
<!-- ══════════════════════════════════════════════════════════════════
PARENTING CLASS (FL 61.21)
══════════════════════════════════════════════════════════════════ -->
<record id="caselaw_maddox_v_maddox_parenting" model="fl.caselaw">
<field name="citation">Maddox v. Maddox, 758 So.2d 1256 (Fla. 3d DCA 2000)</field>
<field name="short_name">Maddox v. Maddox</field>
<field name="court">3rd_dca</field>
<field name="year">2000</field>
<field name="holding">Completion of the court-ordered parenting education course under FL 61.21 is a mandatory prerequisite to entry of a final judgment in cases involving minor children, absent a specific finding of good cause for waiver. The final judgment may not be entered until both parties have completed the course.</field>
<field name="relevance">Both parents must complete the parenting course BEFORE the judge can sign the final order. Do not wait — complete it early to avoid delaying your case.</field>
<field name="favorable_to">neutral</field>
<field name="plain_english">Both you and the other parent must complete a state-approved parenting class before the judge will finalize your case involving children. This is required by law in Florida, not optional. Complete it as soon as possible to avoid delays.</field>
<field name="plain_english_es">Tanto usted como el otro padre deben completar una clase de crianza aprobada por el estado antes de que el juez finalice su caso con niños. Esto es requerido por ley en Florida, no es opcional. Complételo lo antes posible para evitar demoras.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_parenting_class')])]"/>
<field name="active">True</field>
</record>
<!-- ══════════════════════════════════════════════════════════════════
POST-ORDER / MODIFICATION (FL 61.14)
══════════════════════════════════════════════════════════════════ -->
<record id="caselaw_pimm_v_pimm" model="fl.caselaw">
<field name="citation">Pimm v. Pimm, 601 So.2d 534 (Fla. 1992)</field>
<field name="short_name">Pimm v. Pimm</field>
<field name="court">fl_supreme</field>
<field name="year">1992</field>
<field name="holding">The standard for modification of child support under FL 61.14 is a showing of a substantial change in circumstances since the last order. The change must be significant, material, involuntary, and permanent in nature. A temporary income fluctuation does not justify modification.</field>
<field name="relevance">The income change must be real and ongoing — not just a bad month. Job loss can justify modification, but must be involuntary (layoff, not quitting) and expected to last.</field>
<field name="favorable_to">neutral</field>
<field name="plain_english">To change child support, you must show a real, lasting change in circumstances — like losing your job involuntarily or a permanent pay cut. A temporary dip in income, or a voluntary job change to lower pay, typically will not be enough.</field>
<field name="plain_english_es">Para cambiar la manutención infantil, debe mostrar un cambio real y duradero en las circunstancias, como perder su trabajo de manera involuntaria o una reducción permanente de salario. Una caída temporal en los ingresos, o un cambio voluntario de trabajo a menor paga, generalmente no será suficiente.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_modification_threshold'), ref('activeblue_familylaw.tag_post_order')])]"/>
<field name="active">True</field>
</record>
<record id="caselaw_el_kohen_v_el_kohen" model="fl.caselaw">
<field name="citation">El Kohen v. El Kohen, 760 So.2d 318 (Fla. 3d DCA 2000)</field>
<field name="short_name">El Kohen v. El Kohen</field>
<field name="court">3rd_dca</field>
<field name="year">2000</field>
<field name="holding">Retroactivity of a child support modification under FL 61.30(17) is limited to the date of the filing of the motion to modify. A court may not retroactively reduce support for periods before the filing date, and the party seeking modification bears the burden of demonstrating entitlement to retroactive relief.</field>
<field name="relevance">File your modification motion as soon as circumstances change. You can only get a retroactive reduction to the date you filed — not before.</field>
<field name="favorable_to">depends</field>
<field name="plain_english">If you file to reduce child support, the reduction can only go back to the date you actually filed the motion — not to when your income first dropped. File as soon as possible to protect yourself from continuing to owe the old (higher) amount.</field>
<field name="plain_english_es">Si solicita reducir la manutención infantil, la reducción solo puede retrotraerse a la fecha en que realmente presentó la moción, no a cuando sus ingresos cayeron por primera vez. Presente lo antes posible para protegerse de seguir debiendo el monto antiguo (más alto).</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_post_order'), ref('activeblue_familylaw.tag_modification_threshold')])]"/>
<field name="active">True</field>
</record>
<!-- ══════════════════════════════════════════════════════════════════
FL 61.30 GUIDELINES — GENERAL / ABOVE-SCHEDULE
══════════════════════════════════════════════════════════════════ -->
<record id="caselaw_dep_rev_v_smith_highearner" model="fl.caselaw">
<field name="citation">Department of Revenue v. Smith, 717 So.2d 192 (Fla. 3d DCA 1998)</field>
<field name="short_name">Dep't of Revenue v. Smith (Above-Schedule)</field>
<field name="court">3rd_dca</field>
<field name="year">1998</field>
<field name="holding">When combined net income exceeds the schedule maximum ($10,000/month), the court must apply a percentage formula rather than the schedule. The court retains discretion to deviate from the guideline amount with findings, but must begin the calculation using the prescribed formula.</field>
<field name="relevance">When incomes are high (over $10,000/month combined), the guidelines work differently. The court starts with a formula and may deviate with written findings.</field>
<field name="favorable_to">neutral</field>
<field name="plain_english">When both parents together earn more than $10,000 a month, the standard Florida support table doesn't have a number — the court uses a percentage formula instead. This often leads to higher support amounts.</field>
<field name="plain_english_es">Cuando ambos padres juntos ganan más de $10,000 al mes, la tabla estándar de manutención de Florida no tiene un número; el tribunal usa una fórmula de porcentaje en su lugar. Esto a menudo conduce a montos de manutención más altos.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_modification_threshold')])]"/>
<field name="active">True</field>
</record>
<!-- ══════════════════════════════════════════════════════════════════
DISCOVERY / SANCTIONS (FL 1.380)
══════════════════════════════════════════════════════════════════ -->
<record id="caselaw_florida_bar_v_mastos" model="fl.caselaw">
<field name="citation">Mercer v. Raine, 443 So.2d 944 (Fla. 1983)</field>
<field name="short_name">Mercer v. Raine (Discovery Sanctions)</field>
<field name="court">fl_supreme</field>
<field name="year">1983</field>
<field name="holding">Florida courts have broad authority under Rule 1.380 to impose sanctions for discovery violations, including striking pleadings, entering default, excluding evidence, and awarding attorneys' fees. The sanction must be proportionate to the violation.</field>
<field name="relevance">If the other party refuses to produce financial documents, keep asking — and file a Motion to Compel. The court can award you attorney's fees and even strike the other party's pleadings for repeated violations.</field>
<field name="favorable_to">petitioner</field>
<field name="plain_english">If the other parent refuses to hand over financial records (pay stubs, tax returns, bank statements), you can ask the court to order them to do so. If they still refuse, the court can penalize them — including making them pay your legal costs or even entering a judgment against them.</field>
<field name="plain_english_es">Si el otro padre se niega a entregar registros financieros (talones de pago, declaraciones de impuestos, estados de cuenta bancarios), puede pedirle al tribunal que los obligue a hacerlo. Si aún se niegan, el tribunal puede sancionarlos, incluso obligarlos a pagar sus costos legales.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_income_imputation')])]"/>
<field name="active">True</field>
</record>
<!-- ══════════════════════════════════════════════════════════════════
EMANCIPATION / CHILD AGING OUT
══════════════════════════════════════════════════════════════════ -->
<record id="caselaw_dep_rev_emancipation" model="fl.caselaw">
<field name="citation">State, Department of Revenue v. Chambers, 727 So.2d 1053 (Fla. 3d DCA 1999)</field>
<field name="short_name">Dep't of Revenue v. Chambers (Emancipation)</field>
<field name="court">3rd_dca</field>
<field name="year">1999</field>
<field name="holding">Child support terminates automatically upon a child's 18th birthday unless the child is still in high school, in which case support continues until graduation or the child's 19th birthday, whichever occurs first. An affirmative motion or court order is not required for termination due to age, but is required to adjust the amount for remaining children.</field>
<field name="relevance">When a child turns 18 (or graduates high school), you may still owe for any other children on the order. File a modification motion immediately to recalculate for remaining children.</field>
<field name="favorable_to">respondent</field>
<field name="plain_english">When your child turns 18 (or finishes high school, whichever is later), child support for that child automatically ends. BUT if you have other children on the same order, you must file to have the amount recalculated — it does not automatically drop for the remaining children.</field>
<field name="plain_english_es">Cuando su hijo cumple 18 años (o termina la escuela secundaria, lo que sea posterior), la manutención infantil para ese hijo termina automáticamente. PERO si tiene otros hijos en la misma orden, debe presentar una solicitud para que el monto sea recalculado; no cae automáticamente para los hijos restantes.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_post_order')])]"/>
<field name="active">True</field>
</record>
<!-- ══════════════════════════════════════════════════════════════════
FEE WAIVER / INDIGENT STATUS (FL 57.082)
══════════════════════════════════════════════════════════════════ -->
<record id="caselaw_abdool_v_abdool" model="fl.caselaw">
<field name="citation">Abdool v. Boneman, 128 So.3d 915 (Fla. 4th DCA 2013)</field>
<field name="short_name">Abdool v. Boneman (Fee Waiver)</field>
<field name="court">4th_dca</field>
<field name="year">2013</field>
<field name="holding">A determination of indigent status for purposes of fee waiver under FL 57.082 is made by the clerk of court and is reviewable by the trial court. Applicants below 200% of the federal poverty level are presumptively eligible. The clerk's denial is subject to judicial review.</field>
<field name="relevance">If the clerk denies your fee waiver application, you can ask the judge to review that decision. Bring proof of income and household size to the hearing.</field>
<field name="favorable_to">petitioner</field>
<field name="plain_english">If you cannot afford the court filing fee and the clerk denies your fee waiver application, you can ask the judge to review that decision. Bring proof of your income and everyone living in your household to show you qualify.</field>
<field name="plain_english_es">Si no puede pagar la tarifa de presentación del tribunal y el secretario niega su solicitud de exención de tarifas, puede pedirle al juez que revise esa decisión. Traiga prueba de sus ingresos y de todos los que viven en su hogar para mostrar que califica.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_fee_waiver')])]"/>
<field name="active">True</field>
</record>
<!-- ══════════════════════════════════════════════════════════════════
MEDIATION (FL 44.102) — Required before hearing
══════════════════════════════════════════════════════════════════ -->
<record id="caselaw_kielbania_v_kielbania" model="fl.caselaw">
<field name="citation">Kielbania v. Kielbania, 783 So.2d 386 (Fla. 3d DCA 2001)</field>
<field name="short_name">Kielbania v. Kielbania</field>
<field name="court">3rd_dca</field>
<field name="year">2001</field>
<field name="holding">Florida courts may require parties to attend mediation under FL 44.102 before setting a family law matter for trial. Failure to attend court-ordered mediation may result in sanctions. Parties who cannot afford mediator fees may apply for fee reduction or waiver under FL 44.108.</field>
<field name="relevance">Most family courts require mediation before a final hearing. If you cannot afford the mediator, apply for a fee waiver at the same time as your court filing fee waiver.</field>
<field name="favorable_to">neutral</field>
<field name="plain_english">Most family courts require you to try mediation (meeting with a neutral mediator) before a judge hears your case. If you cannot afford the mediator's fee, apply for a reduction or waiver at the same time you apply for the court filing fee waiver.</field>
<field name="plain_english_es">La mayoría de los tribunales de familia requieren que intente la mediación (reunión con un mediador neutral) antes de que un juez escuche su caso. Si no puede pagar la tarifa del mediador, solicite una reducción o exención al mismo tiempo que solicita la exención de la tarifa de presentación del tribunal.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_fee_waiver')])]"/>
<field name="active">True</field>
</record>
<!-- ══════════════════════════════════════════════════════════════════
INCOME WITHHOLDING (FL 61.1301) — Mandatory
══════════════════════════════════════════════════════════════════ -->
<record id="caselaw_dep_rev_withholding" model="fl.caselaw">
<field name="citation">Department of Revenue v. Lopez-Cruz, 809 So.2d 1133 (Fla. 3d DCA 2002)</field>
<field name="short_name">Dep't of Revenue v. Lopez-Cruz (Withholding)</field>
<field name="court">3rd_dca</field>
<field name="year">2002</field>
<field name="holding">Income withholding under FL 61.1301 is mandatory in all child support orders unless the court finds good cause to deviate based on written findings. The withholding order must be served on the employer and is immediately effective. Employer failure to withhold may result in direct liability to the payee.</field>
<field name="relevance">Every child support order must include an income withholding order served on the employer. The employer is legally required to deduct the support from each paycheck and send it to the FL SDU.</field>
<field name="favorable_to">petitioner</field>
<field name="plain_english">Florida law requires that every child support order include an income withholding order that goes directly to the employer. The employer must take the support amount out of every paycheck and send it to the Florida State Disbursement Unit. The paying parent cannot opt out of this.</field>
<field name="plain_english_es">La ley de Florida requiere que cada orden de manutención infantil incluya una orden de retención de ingresos que vaya directamente al empleador. El empleador debe tomar el monto de la manutención de cada cheque de pago y enviarlo a la Unidad de Desembolso del Estado de Florida. El padre que paga no puede optar por no participar en esto.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_post_order')])]"/>
<field name="active">True</field>
</record>
<!-- ══════════════════════════════════════════════════════════════════
RETROACTIVITY (FL 61.30(17))
══════════════════════════════════════════════════════════════════ -->
<record id="caselaw_rolfe_v_rolfe" model="fl.caselaw">
<field name="citation">Rolfe v. Rolfe, 638 So.2d 253 (Fla. 4th DCA 1994)</field>
<field name="short_name">Rolfe v. Rolfe</field>
<field name="court">4th_dca</field>
<field name="year">1994</field>
<field name="holding">Modification of child support may be made retroactive to the date of filing the petition for modification under FL 61.30(17). The court may not award retroactive support for periods before the filing date, but must consider whether retroactivity to the filing date is appropriate given the circumstances.</field>
<field name="relevance">The filing date of your motion is the earliest date any change in support can take effect. File promptly — arrears cannot be forgiven for periods before filing date without consent.</field>
<field name="favorable_to">neutral</field>
<field name="plain_english">Any change in child support — up or down — can go back to the date you filed your court papers, but not before that. If you needed a reduction for the past 6 months but did not file until today, you will owe the full old amount for those 6 months.</field>
<field name="plain_english_es">Cualquier cambio en la manutención infantil, ya sea aumento o disminución, puede retrotraerse a la fecha en que presentó sus documentos del tribunal, pero no antes. Si necesitaba una reducción durante los últimos 6 meses pero no presentó hasta hoy, deberá el monto completo anterior por esos 6 meses.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_post_order'), ref('activeblue_familylaw.tag_modification_threshold')])]"/>
<field name="active">True</field>
</record>
<!-- ══════════════════════════════════════════════════════════════════
FINANCIAL DISCLOSURE / MANDATORY DISCLOSURE (FL 12.285)
══════════════════════════════════════════════════════════════════ -->
<record id="caselaw_dep_rev_disclosure" model="fl.caselaw">
<field name="citation">Department of Revenue v. Cummings, 871 So.2d 1076 (Fla. 3d DCA 2004)</field>
<field name="short_name">Dep't of Revenue v. Cummings (Disclosure)</field>
<field name="court">3rd_dca</field>
<field name="year">2004</field>
<field name="holding">Mandatory disclosure under Rule 12.285 requires the exchange of financial documents within 45 days of service of the initial petition. Failure to comply may result in sanctions including striking pleadings, exclusion of evidence, or contempt. The documents must be actually exchanged — filing with the court alone is insufficient.</field>
<field name="relevance">You must exchange financial documents with the other party within 45 days of service. Keep a record of what you sent and when (certified mail or email with receipts).</field>
<field name="favorable_to">neutral</field>
<field name="plain_english">Within 45 days of the other parent receiving your court papers, BOTH parents must share their financial documents (tax returns, pay stubs, bank statements). You must actually send them to the other person — putting them in the court file alone is not enough. Keep proof that you sent them.</field>
<field name="plain_english_es">Dentro de los 45 días posteriores a que el otro padre reciba sus documentos del tribunal, AMBOS padres deben compartir sus documentos financieros (declaraciones de impuestos, talones de pago, estados de cuenta bancarios). Debe enviarlos realmente a la otra persona; poner solo en el archivo del tribunal no es suficiente. Guarde prueba de que los envió.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_income_imputation')])]"/>
<field name="active">True</field>
</record>
<!-- ══════════════════════════════════════════════════════════════════
BEST INTERESTS OF CHILD (FL 61.13(3))
══════════════════════════════════════════════════════════════════ -->
<record id="caselaw_boykin_v_boykin" model="fl.caselaw">
<field name="citation">Boykin v. Boykin, 843 So.2d 317 (Fla. 3d DCA 2003)</field>
<field name="short_name">Boykin v. Boykin</field>
<field name="court">3rd_dca</field>
<field name="year">2003</field>
<field name="holding">In establishing a parenting plan, the court must evaluate the 20 best interest factors set forth in FL 61.13(3). No single factor is determinative, and the court must consider the totality of the circumstances. The child's primary bond and established routine are important considerations.</field>
<field name="relevance">To argue for more timesharing, you must address multiple factors from the FL 61.13(3) list. Bring evidence for each one that is relevant to your situation (school records, medical records, a log of your time with the children).</field>
<field name="favorable_to">depends</field>
<field name="plain_english">When a judge decides where children should live and how often each parent sees them, the judge must consider 20 different factors about what is best for the children — not just what each parent wants. Come to court prepared to show evidence for each factor that applies to your situation.</field>
<field name="plain_english_es">Cuando un juez decide dónde deben vivir los niños y con qué frecuencia cada padre los ve, el juez debe considerar 20 factores diferentes sobre lo que es mejor para los niños, no solo lo que cada padre quiere. Venga al tribunal preparado para mostrar evidencia de cada factor que aplica a su situación.</field>
<field name="issue_tag_ids" eval="[(6, 0, [ref('activeblue_familylaw.tag_timesharing_deviation'), ref('activeblue_familylaw.tag_parenting_class')])]"/>
<field name="active">True</field>
</record>
</data>
</odoo>

View File

@@ -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
)

View File

@@ -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