diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/__manifest__.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/__manifest__.py index 655267c..dacfaee 100644 --- a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/__manifest__.py +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/__manifest__.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- { "name": "Active Blue Family Law", - "version": "18.0.6.0.0", + "version": "18.0.7.0.0", "category": "Services/Legal", "summary": "Florida family law case management (Miami-Dade / 11th Judicial Circuit)", "description": """ diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/__init__.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/__init__.py index 3ce3e32..5590df7 100644 --- a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/__init__.py +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/__init__.py @@ -10,4 +10,6 @@ from . import familylaw_deadline from . import familylaw_disclosure from . import familylaw_ai from . import familylaw_citation +from . import familylaw_verifier +from . import familylaw_research from . import familylaw_ai_wizard diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_citation.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_citation.py index 3839741..e7a42cb 100644 --- a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_citation.py +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_citation.py @@ -12,6 +12,9 @@ hallucinated cites — this ledger is what makes that mechanically impossible. """ from odoo import api, fields, models, _ +from odoo.exceptions import UserError + +ATTORNEY_GROUP = "activeblue_familylaw.group_familylaw_attorney" class FamilyLawCitation(models.Model): @@ -61,7 +64,43 @@ class FamilyLawCitation(models.Model): ) source_ai_task_id = fields.Many2one("familylaw.ai.task", string="Proposed By (AI Task)") + verification_source = fields.Char(string="Verified Via", readonly=True) + verification_date = fields.Datetime(string="Verified On", readonly=True) + verification_note = fields.Text(string="Verification Note", readonly=True) + @api.depends("status") def _compute_is_verified(self): for cite in self: cite.is_verified = cite.status == "verified" + + # --- verification (Gate 2 engine) --------------------------------------- + def action_verify(self): + """Run the fail-closed verifier against each citation.""" + verifier = self.env["familylaw.citation.verifier"] + for cite in self: + verifier.verify_citation(cite) + return True + + def action_attorney_verify(self): + """Attorney override: the licensed attorney attests the cite is good law. + Still a deliberate, audited human act — not an automatic pass.""" + if not self.env.user.has_group(ATTORNEY_GROUP): + raise UserError( + _("Only a licensed attorney may manually attest a citation.") + ) + for cite in self: + cite.write({ + "status": "verified", + "verification_source": "attorney", + "verification_date": fields.Datetime.now(), + "verification_note": _("Manually attested by %s.") % self.env.user.name, + }) + cite.message_post(body=_("Citation attested by attorney %s.") + % self.env.user.name) + return True + + def action_reject(self): + for cite in self: + cite.status = "rejected" + cite.message_post(body=_("Citation rejected.")) + return True diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_document.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_document.py index 9443c5b..6050516 100644 --- a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_document.py +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_document.py @@ -109,6 +109,17 @@ class FamilyLawDocument(models.Model): citation_ids = fields.One2many( "familylaw.citation", "document_id", string="Citations", ) + unverified_citation_count = fields.Integer( + compute="_compute_unverified_citation_count", + string="Unverified Citations", + ) + + @api.depends("citation_ids.status") + def _compute_unverified_citation_count(self): + for doc in self: + doc.unverified_citation_count = len( + doc.citation_ids.filtered(lambda c: c.status != "verified") + ) # --- gate helpers ------------------------------------------------------- def _ensure_attorney(self): @@ -130,6 +141,41 @@ class FamilyLawDocument(models.Model): state=not_approved[0].state) ) + def _ensure_citations_verified(self): + """Gate 2: refuse filing/sending if ANY citation is not verified. + + Born-unverified, fail-closed verification (Step 7) makes an AI-hallucinated + cite mechanically unable to reach a filing.""" + for doc in self: + bad = doc.citation_ids.filtered(lambda c: c.status != "verified") + if bad: + raise UserError( + _("Document '%(name)s' has %(n)d unverified citation(s). Every " + "citation must be verified against a real reporter before the " + "document can be filed or sent (the citation gate). Unverified: " + "%(list)s", + name=doc.name, n=len(bad), + list="; ".join(bad.mapped("citation_text"))) + ) + + def action_verify_citations(self): + """Run the fail-closed verifier over every citation on this document.""" + for doc in self: + doc.citation_ids.action_verify() + return True + + def action_request_filing(self): + """Validate BOTH gates and flag the document ready for filing prep. + Available to staff to assemble; the actual filing (action_mark_filed) stays + attorney-only.""" + self._ensure_approved() + self._ensure_citations_verified() + for doc in self: + doc.message_post( + body=_("Filing requested — review gate and citation gate both passed.") + ) + return True + # --- review workflow ---------------------------------------------------- def action_submit_for_review(self): for doc in self: @@ -240,7 +286,8 @@ class FamilyLawDocument(models.Model): # --- outbound actions (Gate 1 enforced) --------------------------------- def action_mark_filed(self): self._ensure_attorney() - self._ensure_approved() + self._ensure_approved() # Gate 1 + self._ensure_citations_verified() # Gate 2 for doc in self: doc.state = "filed" doc.message_post(body=_("Marked as filed by %s.") % self.env.user.name) @@ -248,7 +295,8 @@ class FamilyLawDocument(models.Model): def action_mark_sent(self): self._ensure_attorney() - self._ensure_approved() + self._ensure_approved() # Gate 1 + self._ensure_citations_verified() # Gate 2 for doc in self: doc.state = "sent" doc.message_post(body=_("Marked as sent by %s.") % self.env.user.name) diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_research.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_research.py new file mode 100644 index 0000000..3d6a003 --- /dev/null +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_research.py @@ -0,0 +1,98 @@ +# -*- coding: utf-8 -*- +"""STEP 7 — Research loop (search -> propose citations -> verify each -> synthesize). + +The design allows the agentic research chain to run either in an external FastAPI +orchestrator OR as sequenced steps inside Odoo (an explicitly acceptable +alternative). This implements the in-Odoo path so the platform is self-contained; +the FastAPI orchestrator can replace run() later without changing callers. + +TIER 3: every citation is born unverified and verified FAIL-CLOSED before the +synthesized memo can be filed; the memo itself is born ai_draft for attorney review. +Nothing here is authoritative. +""" + +from odoo import api, fields, models, _ + + +class FamilyLawResearchAgent(models.AbstractModel): + _name = "familylaw.research.agent" + _description = "Case-law research loop" + + @api.model + def run(self, proceeding, question): + """Run the loop and return the synthesized DRAFT document. + + 1. PROPOSE — ask the model for candidate authority (citations born unverified) + 2. VERIFY — run each through the fail-closed verifier + 3. SYNTHESIZE — draft a memo; the draft is born ai_draft (attorney review) + + Both model calls go through familylaw.ai.client (mocked in tests); the + verifier's network calls are mocked too. + """ + proceeding = proceeding if hasattr(proceeding, "id") else \ + self.env["familylaw.proceeding"].browse(proceeding) + Client = self.env["familylaw.ai.client"] + Doc = self.env["familylaw.document"] + Cit = self.env["familylaw.citation"] + verifier = self.env["familylaw.citation.verifier"] + + # 1. PROPOSE authority + propose_msg = [{ + "role": "user", + "content": _("Propose candidate Florida authority for: %s. Return " + "citations for the attorney to verify — do not assert good " + "law yourself.") % question, + }] + proposal, propose_task = Client.generate( + "research_propose", propose_msg, + case=proceeding.case_id, proceeding=proceeding, + name=_("Research: propose authority"), + ) + + # Synthesis document is born ai_draft; citations attach to it. + doc = Doc.create({ + "name": _("Research Memo — %s") % (question[:60] or "untitled"), + "proceeding_id": proceeding.id, + "document_type": "other", + "source": "ai", + "state": "ai_draft", + }) + propose_task.document_id = doc.id + + citations = self.env["familylaw.citation"] + for c in (proposal.get("citations") or []): + cite = Cit.create({ + "document_id": doc.id, + "citation_text": c.get("citation_text") or c.get("text") or _("(unparsed)"), + "case_name": c.get("case_name"), + "proposition": c.get("proposition"), + "status": "unverified", + "source_ai_task_id": propose_task.id, + }) + citations |= cite + + # 2. VERIFY each (fail-closed) + for cite in citations: + verifier.verify_citation(cite) + + # 3. SYNTHESIZE using ONLY verified authority + verified = citations.filtered(lambda c: c.status == "verified") + verified_text = "\n".join(verified.mapped("citation_text")) or _("(none verified)") + synth_msg = [{ + "role": "user", + "content": _("Draft a research memo answering: %(q)s. Use ONLY these " + "verified citations:\n%(c)s", q=question, c=verified_text), + }] + synthesis, synth_task = Client.generate( + "research_synthesize", synth_msg, + case=proceeding.case_id, proceeding=proceeding, + name=_("Research: synthesize memo"), + ) + synth_task.document_id = doc.id + doc.body = synthesis.get("text") or "" + doc.message_post(body=_( + "Research loop complete: %(p)d proposed, %(v)d verified. Memo is a DRAFT " + "for attorney review; unverified citations block filing.", + p=len(citations), v=len(verified), + )) + return doc diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_verifier.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_verifier.py new file mode 100644 index 0000000..d7a1593 --- /dev/null +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/models/familylaw_verifier.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- +"""STEP 7 — Citation verification (Gate 2 engine), FAIL-CLOSED + fallback source. + +CourtListener is a single point of failure for the citation gate — the thing that +prevents AI-hallucinated-cite sanctions. So this verifier: + + 1. FAILS CLOSED — if the source is down, rate-limited, or has no record, the + citation NEVER becomes 'verified'. It stays blocked. We only set 'verified' + when a source AFFIRMATIVELY reports the case exists and is good law. + 2. Has a FALLBACK source — if the primary (CourtListener) is unavailable, a + secondary source is consulted before giving up. + 3. FL coverage caveat — confirm CourtListener (+fallback) actually cover Florida + DCA + Supreme Court family-law authority before relying on this in production. + +The two network methods (_verify_courtlistener / _verify_fallback) are the ONLY +places that touch the network and are the mock points in tests. +""" + +import logging + +from odoo import api, fields, models, _ + +_logger = logging.getLogger(__name__) + +PARAM_CL_TOKEN = "familylaw.courtlistener_token" +PARAM_CL_ENDPOINT = "familylaw.courtlistener_endpoint" +DEFAULT_CL_ENDPOINT = "https://www.courtlistener.com/api/rest/v4/citation-lookup/" + + +class FamilyLawCitationVerifier(models.AbstractModel): + _name = "familylaw.citation.verifier" + _description = "Citation Verifier (fail-closed)" + + # --- network methods (MOCK THESE IN TESTS) ------------------------------ + def _verify_courtlistener(self, citation_text): + """Primary source. Returns a dict {found, good_law, note} or raises on a + transport/rate-limit error (which the caller treats as 'unavailable'). + Returning {'found': False} means the source AFFIRMATIVELY has no record.""" + import requests + icp = self.env["ir.config_parameter"].sudo() + endpoint = icp.get_param(PARAM_CL_ENDPOINT) or DEFAULT_CL_ENDPOINT + token = icp.get_param(PARAM_CL_TOKEN) + headers = {} + if token: + headers["Authorization"] = "Token %s" % token + resp = requests.post( + endpoint, headers=headers, data={"text": citation_text}, timeout=60, + ) + resp.raise_for_status() + data = resp.json() + # CourtListener returns a list of matches; a non-empty match => found. + matches = data if isinstance(data, list) else data.get("results", []) + found = bool(matches) + # good-law / treatment is not a simple flag in the public API; default to + # found-implies-citable but flag for attorney. Conservative: good_law=found. + return { + "found": found, + "good_law": found, + "note": _("CourtListener returned %d match(es).") % len(matches or []), + } + + def _verify_fallback(self, citation_text): + """Secondary source (e.g. a secondary reporter API / Google Scholar gateway). + Same contract as the primary. Not wired to a real provider by default — + returns None ('no fallback configured') so the verifier fails closed.""" + return None + + # --- decision (fail-closed) --------------------------------------------- + def verify_citation(self, citation): + text = citation.citation_text or "" + result = None + used_source = False + + # primary + try: + result = self._verify_courtlistener(text) + used_source = "courtlistener" + except Exception as exc: # noqa: BLE001 + _logger.warning("CourtListener unavailable for '%s': %s", text, exc) + result = None + + # fallback only if primary unavailable or reported no record + if result is None or not result.get("found"): + try: + fb = self._verify_fallback(text) + except Exception as exc: # noqa: BLE001 + _logger.warning("Fallback verifier unavailable: %s", exc) + fb = None + if fb is not None and (result is None or fb.get("found")): + result = fb + used_source = "fallback" + + # decide — only an affirmative found+good_law yields 'verified' + if result is None: + status = "unverified" + note = _("Verification source(s) unavailable — FAILED CLOSED (stays blocked).") + source = False + elif result.get("found") and result.get("good_law"): + status = "verified" + note = result.get("note") or _("Verified.") + source = used_source + elif result.get("found") and not result.get("good_law"): + status = "rejected" + note = result.get("note") or _("Found but flagged as not good law.") + source = used_source + else: + status = "not_found" + note = result.get("note") or _("No record found — stays blocked.") + source = used_source + + citation.write({ + "status": status, + "verification_source": source or False, + "verification_note": note, + "verification_date": fields.Datetime.now(), + }) + citation.message_post( + body=_("Verification result: %(s)s (%(src)s). %(note)s", + s=status, src=source or _("none"), note=note) + ) + return status diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/__init__.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/__init__.py index acae904..1ee1e62 100644 --- a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/__init__.py +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/__init__.py @@ -4,3 +4,4 @@ from . import test_step3 from . import test_step4 from . import test_step5 from . import test_step6 +from . import test_step7 diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/test_step7.py b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/test_step7.py new file mode 100644 index 0000000..255881a --- /dev/null +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/tests/test_step7.py @@ -0,0 +1,219 @@ +# -*- coding: utf-8 -*- +"""STEP 7 tests — citation gate (Gate 2): fail-closed verification + filing block. + + odoo -d -u activeblue_familylaw --test-enable \ + --test-tags familylaw_step7 --stop-after-init + +External calls ALWAYS mocked: the verifier's _verify_courtlistener / _verify_fallback +and the AI client's _call_provider. Tests assert on Odoo state: + * verified only on an affirmative found+good_law; + * primary down -> fallback used; + * both down -> stays unverified (FAIL CLOSED); + * not found -> not_found; bad law -> rejected; + * filing BLOCKED on any unverified citation, allowed when all verified; + * research loop produces an ai_draft memo and verifies proposed citations. +""" + +from unittest.mock import patch + +from odoo.tests.common import TransactionCase, new_test_user, tagged +from odoo.exceptions import UserError + +VERIFIER = ("odoo.addons.activeblue_familylaw.models.familylaw_verifier." + "FamilyLawCitationVerifier") +CLIENT = "odoo.addons.activeblue_familylaw.models.familylaw_ai.FamilyLawAIClient" + + +def _ai_response(text="MEMO", citations=None, pt=10, ct=20): + return {"text": text, "usage": {"input_tokens": pt, "output_tokens": ct}, + "citations": citations or []} + + +@tagged("post_install", "-at_install", "familylaw", "familylaw_step7") +class TestStep7CitationGate(TransactionCase): + + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.attorney = new_test_user( + cls.env, login="fl_atty7", name="Attorney 7", + email="atty7@example.com", + groups="base.group_user,activeblue_familylaw.group_familylaw_attorney", + ) + cls.partner = cls.env["res.partner"].create({"name": "Cite Client"}) + cls.case = cls.env["familylaw.case"].create({ + "name": "Cite Matter", "client_id": cls.partner.id, + "case_type": "support_modification", + }) + cls.proc = cls.case.proceeding_ids[0] + cls.Doc = cls.env["familylaw.document"] + cls.Cit = cls.env["familylaw.citation"] + icp = cls.env["ir.config_parameter"].sudo() + icp.set_param("familylaw.anthropic_api_key", "k") + icp.set_param("familylaw.model", "claude-sonnet-4-6") + + def _doc(self): + return self.Doc.create({ + "name": "Brief", "proceeding_id": self.proc.id, + "document_type": "pleading", "source": "manual", + }) + + def _cite(self, doc, text="Smith v. Jones, 1 So.3d 2 (Fla. 3d DCA 2010)"): + return self.Cit.create({"document_id": doc.id, "citation_text": text}) + + # --- verification decisions --------------------------------------------- + def test_01_primary_found_good_law_verified(self): + doc = self._doc(); cite = self._cite(doc) + with patch(VERIFIER + "._verify_courtlistener", + return_value={"found": True, "good_law": True, "note": "ok"}): + cite.action_verify() + self.assertEqual(cite.status, "verified") + self.assertEqual(cite.verification_source, "courtlistener") + + def test_02_not_found_stays_blocked(self): + doc = self._doc(); cite = self._cite(doc) + with patch(VERIFIER + "._verify_courtlistener", + return_value={"found": False, "good_law": False}), \ + patch(VERIFIER + "._verify_fallback", return_value=None): + cite.action_verify() + self.assertEqual(cite.status, "not_found") + self.assertFalse(cite.is_verified) + + def test_03_bad_law_rejected(self): + doc = self._doc(); cite = self._cite(doc) + with patch(VERIFIER + "._verify_courtlistener", + return_value={"found": True, "good_law": False}): + cite.action_verify() + self.assertEqual(cite.status, "rejected") + + # --- fail closed -------------------------------------------------------- + def test_04_primary_down_no_fallback_stays_unverified(self): + doc = self._doc(); cite = self._cite(doc) + + def _down(*a, **k): + raise ConnectionError("CourtListener 503") + + with patch(VERIFIER + "._verify_courtlistener", _down), \ + patch(VERIFIER + "._verify_fallback", return_value=None): + cite.action_verify() + self.assertEqual(cite.status, "unverified") # FAIL CLOSED + self.assertFalse(cite.is_verified) + + def test_05_primary_down_fallback_verifies(self): + doc = self._doc(); cite = self._cite(doc) + + def _down(*a, **k): + raise ConnectionError("down") + + with patch(VERIFIER + "._verify_courtlistener", _down), \ + patch(VERIFIER + "._verify_fallback", + return_value={"found": True, "good_law": True, "note": "fb"}): + cite.action_verify() + self.assertEqual(cite.status, "verified") + self.assertEqual(cite.verification_source, "fallback") + + def test_06_both_down_fail_closed(self): + doc = self._doc(); cite = self._cite(doc) + + def _down(*a, **k): + raise ConnectionError("down") + + with patch(VERIFIER + "._verify_courtlistener", _down), \ + patch(VERIFIER + "._verify_fallback", side_effect=_down): + cite.action_verify() + self.assertEqual(cite.status, "unverified") + + # --- attorney attestation ----------------------------------------------- + def test_07_attorney_attest(self): + doc = self._doc(); cite = self._cite(doc) + cite.with_user(self.attorney).action_attorney_verify() + self.assertEqual(cite.status, "verified") + self.assertEqual(cite.verification_source, "attorney") + + def test_08_attest_requires_attorney(self): + doc = self._doc(); cite = self._cite(doc) + para = new_test_user( + self.env, login="fl_para7", name="Para 7", email="p7@example.com", + groups="base.group_user,activeblue_familylaw.group_familylaw_user", + ) + with self.assertRaises(UserError): + cite.with_user(para).action_attorney_verify() + + # --- THE FILING GATE (Gate 2) ------------------------------------------- + def _approve(self, doc): + doc.action_submit_for_review() + doc.with_user(self.attorney).action_approve() + + def test_09_filing_blocked_with_unverified_cite(self): + doc = self._doc() + self._cite(doc) # unverified + self._approve(doc) + with self.assertRaises(UserError): + doc.with_user(self.attorney).action_mark_filed() + self.assertNotEqual(doc.state, "filed") + + def test_10_filing_allowed_when_all_verified(self): + doc = self._doc() + cite = self._cite(doc) + with patch(VERIFIER + "._verify_courtlistener", + return_value={"found": True, "good_law": True}): + cite.action_verify() + self._approve(doc) + doc.with_user(self.attorney).action_mark_filed() + self.assertEqual(doc.state, "filed") + + def test_11_request_filing_blocked_unverified(self): + doc = self._doc() + self._cite(doc) + self._approve(doc) + with self.assertRaises(UserError): + doc.action_request_filing() + + def test_12_doc_with_no_citations_can_file(self): + doc = self._doc() + self._approve(doc) + doc.with_user(self.attorney).action_mark_filed() + self.assertEqual(doc.state, "filed") + + def test_13_send_blocked_with_unverified_cite(self): + doc = self._doc() + self._cite(doc) + self._approve(doc) + with self.assertRaises(UserError): + doc.with_user(self.attorney).action_mark_sent() + + def test_14_unverified_count(self): + doc = self._doc() + c1 = self._cite(doc, "A v. B") + self._cite(doc, "C v. D") + self.assertEqual(doc.unverified_citation_count, 2) + with patch(VERIFIER + "._verify_courtlistener", + return_value={"found": True, "good_law": True}): + c1.action_verify() + self.assertEqual(doc.unverified_citation_count, 1) + + # --- research loop ------------------------------------------------------ + def test_15_research_loop_produces_ai_draft(self): + cites = [{"citation_text": "Doe v. Roe, 9 So.3d 9 (Fla. 2012)", + "proposition": "p"}] + with patch(CLIENT + "._call_provider", + return_value=_ai_response(citations=cites)), \ + patch(VERIFIER + "._verify_courtlistener", + return_value={"found": True, "good_law": True}): + doc = self.env["familylaw.research.agent"].run(self.proc, "test question") + self.assertEqual(doc.state, "ai_draft") + self.assertEqual(len(doc.citation_ids), 1) + self.assertEqual(doc.citation_ids.status, "verified") + + def test_16_research_loop_unverified_blocks_filing(self): + cites = [{"citation_text": "Fake v. Hallucination, 0 So.0d 0", "proposition": "p"}] + with patch(CLIENT + "._call_provider", + return_value=_ai_response(citations=cites)), \ + patch(VERIFIER + "._verify_courtlistener", + return_value={"found": False, "good_law": False}), \ + patch(VERIFIER + "._verify_fallback", return_value=None): + doc = self.env["familylaw.research.agent"].run(self.proc, "q") + self.assertEqual(doc.citation_ids.status, "not_found") + self._approve(doc) + with self.assertRaises(UserError): + doc.with_user(self.attorney).action_mark_filed() diff --git a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/views/familylaw_ai_views.xml b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/views/familylaw_ai_views.xml index 2a853ec..90d21f3 100644 --- a/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/views/familylaw_ai_views.xml +++ b/activeblue_familylaw_handoff/activeblue_familylaw_build/activeblue_familylaw/views/familylaw_ai_views.xml @@ -89,6 +89,17 @@ familylaw.citation
+
+