Commit Graph

14 Commits

Author SHA1 Message Date
chaim 7b517e12b3 fix(ocr+safety): stop document-content hallucinations from empty OCR
Three independent bugs combined to make Shira fabricate the content of
case-46-Friedman's appeal in production (CTS case rewritten as a knee
injury, wrong dates, wrong court file number, wrong %-of-disability —
written into save_memory/Task/Meeting on prod).

* api/services/ocr.py — ocr_docx_images now reads word/document.xml first
  (text content), then OCRs embedded images. Previously it only OCR'd the
  word/media/* images, so a text-heavy DOCX with one signature image
  returned only "[signature]" to the LLM. Verified against the same
  Friedman appeal: 16633 chars / 80 paragraphs / 0 XML noise.

* mcp_server/tools/document_tools.py — _ocr_fallback now refuses to wrap
  an empty/signature-only OCR result as success. If <80 useful chars or
  just "[signature]", returns an explicit failure that instructs the LLM
  not to describe / summarize the document.

* api/services/prompt_builder.py — TOOL_RULES adds the absolute rule
  "DOCUMENT FAITHFULNESS": never quote a document not literally in the
  tool result for THIS turn; never infer content from a file name; treat
  dates and case numbers as especially dangerous to invent.

Refs Task Master #5, #6, #7

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 13:58:37 +00:00
chaim 90c60f45e6 feat: 9 new tools — document creation, folder management, CRM updates
Adds the Python side of the SmartAssistant 2.9 release. Each tool maps to
either a new EspoCRM action or a generic CRM endpoint.

Document creation:
- create_document: free-form RTL DOCX with markdown body; calls
  SmartAssistant/action/createDocument.

Folder management (all sandboxed to the current case's root):
- write_document_to_folder: raw upload to a path inside the case folder.
- create_subfolder: mkdir with recursive parent creation.
- rename_item / move_item: rename/move files and folders.
- mark_document_for_deletion: soft-delete — moves the file to
  "מסמכים למחיקה/" with a "למחיקה - " prefix. Shira is explicitly told
  she has NO physical-delete permission.

CRM gaps:
- update_case_fields: allow-listed PUT on Case (judge, court, caseType,
  practiceArea, opposingParty, status, priority, etc.).
- search_entities: generic GET with EspoCRM where[] params across
  Case/Contact/Account/Document/Task. Wildcard "*" maps to LIKE.
- draft_email: POST Email with status="Draft", linked to the current case.

prompt_builder additions:
- FREE DOCUMENT CREATION: don't say "I can't" — call create_document.
- FOLDER MANAGEMENT: full read/write inside the case folder only.
- FILE DELETION: always mark_document_for_deletion; never claim a physical
  delete.

Pairs with SmartAssistant extension release that adds FreeDocumentGenerator,
CaseFolderManager, and 6 controller actions.

Refs Task Master #4

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-14 13:03:59 +00:00
chaim 206a800762 feat(billing): teach Shira the GreenInvoiceBilling extension surface
Adds two new tool categories — billing (10 tools) and legal_aid (6 tools)
— so Shira can drive the full Green Invoice flow: list unbilled activities,
manage charges (approve/cancel/calculate), build draft invoices from
activities, sync to Green Invoice, plus the legal-aid sub-flow (proforma,
mark-submitted, monthly 320 wrap-up, prepare-email, resubmit rejected).

Wires both registrars into agent_runner with allowed_toolsets gating, and
extends TOOL_RULES so Shira (a) shows draft totals before creating, (b)
treats send_invoice_to_green_invoice as irreversible and demands explicit
confirmation, and (c) never marks a legal-aid proforma as submitted
without an explicit request number from the user.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-29 16:32:29 +00:00
chaim f03721e801 feat(kb): multi-topic foundation — kb_topic table, topic-scoped endpoints
Phase 1 of the multi-domain refactor. Adds a kb_topic table seeded with
the existing 'ביטוח לאומי' domain (id=1) and pins all 6 existing
kb_source rows to it, so search/ask can be scoped per domain without
disturbing the current single-topic UX.

- scripts/migrations/001_topics.sql: idempotent migration. Creates
  kb_topic, seeds national-insurance with the system_prompt_addendum
  copied verbatim from kb_public.py's hardcoded text, adds nullable
  kb_source.topic_id, backfills it to 1, then SET NOT NULL. GRANTs to
  shira_kb mirror its existing kb_source privileges. Wrapped in BEGIN
  /COMMIT with a sanity-check that raises if the backfill is incomplete.
- api/services/kb/topics.py: list_topics / get_topic / resolve_topic_id.
  resolve_topic_id raises ValueError on unknown ids so the route layer
  can return 400 instead of silently mixing domains.
- api/services/kb/search.py: search() + _retrieve_rrf accept topic_id;
  the SQL adds AND s.topic_id = $N when present. _expand_query is
  parametrized by topic name (was hardcoded "הביטוח הלאומי").
- api/routes/kb_public.py: new GET /kb/topics. /kb/search /kb/sources
  /kb/ask /kb/ask/stream all accept topic_id and call resolve_topic_id.
  _build_ask_runner_context loads system_prompt_addendum from the DB
  instead of the hardcoded insurance string.
- mcp_server/tools/legal_kb_tools.py: tool renamed
  search_insurance_kb → search_legal_kb, takes topic_id at registration
  so each conversation is pinned to its caller-chosen domain. Tool
  description interpolates the topic name.
- api/services/agent_runner.py: kb_topic_id + kb_topic_name flow
  through to register_legal_kb_tools. Hebrew progress labels updated.

Refs Task Master #12 (espocrm-extensions/KnowledgeBase).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 14:15:19 +00:00
chaim 4a3a8945af feat(kb): track page_number per chunk + expose sources_used from /kb/ask
To let the browse/ask UIs jump straight to the right PDF page:

- ingest.py: embed \x00KB_PAGE:<N>\x00 sentinels at the start of each
  PDF page during _parse_pdf.
- chunker.py: _strip_page_markers removes the sentinels and records the
  first page seen per chunk as page_number. Chunk dataclass + _as_dicts
  propagate page_number through both chunk_statute and chunk_circular.
- ingest.py INSERT: new kb_chunk.page_number column (DB migration
  applied manually).
- search.py SQL: SELECT c.page_number + s.id + s.original_path so the
  rerank results carry them forward.
- legal_kb_tools.py: register_legal_kb_tools(sources_used=list) — the
  tool appends {source_id, title, kind, identifier, heading_path,
  section_ref, page_number, original_path} for each hit, deduped by
  (source_id, page_number).
- agent_runner.py: new kb_sources_used kwarg threads the list through
  to the tool registration.
- kb_public.py /kb/ask: collects sources_used during the run, filters
  to PDF-only sources, and returns them alongside text.

Chunks stored before this change have page_number = NULL; PDFs need to
be re-ingested to populate them. The TXT law source is unaffected.

Refs Task Master #2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 12:20:52 +00:00
chaim 3d5bc38401 feat(kb): per-document metadata via sidecar JSON
Each file in the inbox can now be accompanied by a `<filename>.meta.json`
sidecar that supplies identifier, published_at, effective_at, and
source_url. The scanner applies filename-derived defaults first and lets
the sidecar override individual fields.

- list_inbox / list_failed skip `.meta.json` so sidecars aren't ingested
  as standalone documents.
- mark_processed, mark_failed, and requeue-failed move the sidecar
  alongside its parent (best-effort).
- mark_failed now sanitizes the error string to ASCII before writing it
  to S3 object metadata (HTTP header encoding).
- search SQL selects published_at/effective_at; the formatter shows the
  full heading_path plus publication date so citations are unambiguous.
- scripts/kb_sidecar_template.json documents the expected shape.

Refs Task Master #2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 16:12:32 +00:00
chaim 9edcb58c93 feat(kb): Phase 1 — Israeli National Insurance knowledge base
Adds a searchable KB for חוק הביטוח הלאומי, תקנות הביטוח הלאומי, and חוזרי
הביטוח הלאומי. Hybrid search (pgvector cosine + tsvector/trgm) fused with
Reciprocal Rank Fusion, exposed to Shira as the `search_insurance_kb` tool.

- api/services/kb/: asyncpg pool, Voyage voyage-multilingual-2 client,
  per-kind chunker (statute-by-section / circular header-aware), ingest
  pipeline with supersession (old source → superseded_by), hybrid search.
- api/routes/admin_kb.py: POST /admin/kb/ingest (multipart), GET /admin/kb/stats.
- mcp_server/tools/legal_kb_tools.py: `search_insurance_kb` registered under
  the `legal` toolset.
- pyproject.toml: +asyncpg, +python-docx, +boto3, +python-multipart.

Infra (external): pgvector/pgvector:pg16 on the shared PG, insurance_kb DB
with hnsw + gin indexes, MinIO bucket insurance-kb, KB_DATABASE_URL in
Infisical /espocrm, VOYAGE_API_KEY/VOYAGE_MODEL/KB_DATABASE_URL on the
shira-hermes Coolify app.

Refs Task Master #2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 14:36:09 +00:00
chaim 84269a8d51 feat: add signature tools for digital signature integration
Adds 4 Python tool registrations that call EspoCRM's SmartAssistant
executeTool endpoint with the DigitalSignature plugin tools:

- send_for_signature — send document for signing with auto-resolved signers
- check_signature_status — query status of signature requests
- remind_signers — send WhatsApp reminders to pending signers
- list_pending_signatures — show all awaiting signature requests

Tools are registered under the "signature" toolset, enabled by default.
Requires DigitalSignature extension v2.0.0+ on the EspoCRM side.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-20 11:06:21 +00:00
chaim 51c76c1cf9 feat: Claude Vision OCR fallback for unreadable documents
When the CRM's readDocument returns success:false (scanned PDFs,
image-only DOCX, corrupted files), shira-hermes now:

1. Fetches the raw file bytes via new getDocumentBytes endpoint
2. Sends them to Claude Vision through ai-gateway:
   - Images (PNG/JPG/TIFF/etc): direct vision call
   - PDFs: render each page at 180 DPI via PyMuPDF, vision call per page
   - DOCX: extract images from word/media/, vision call per image

New tool: read_document_ocr (explicit OCR request)
Existing tool read_document auto-falls-back to OCR on extraction failure.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 11:37:51 +00:00
chaim 5086fd48b8 fix: list_documents crashes when context.documents is a list
When EspoCRM returns documents as a flat list (legacy shape) instead of
the expected {totalFiles, folders, ...} dict, list_documents crashed with
"list object has no attribute 'get'". Now handles both shapes gracefully.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-19 11:00:23 +00:00
chaim a0c2211399 fix: convert UTC dates to Israel timezone in display and input
EspoCRM stores dates in UTC. Shira was displaying them raw (e.g., 08:00 UTC
instead of 11:00 Israel time) and storing user-provided local times as UTC.

- Add _to_local() in prompt_builder to convert UTC→Asia/Jerusalem for all
  displayed dates (hearings, meetings, calls, tasks, notes)
- Update normalize_date() in _helpers to convert Israel local→UTC before
  sending to EspoCRM

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 04:09:43 +00:00
chaim e947bca655 fix: code audit — security, correctness, and robustness fixes
Security:
- Path traversal: validate user_id, case_id, skill_name against safe regex
- Input sanitization in memory.py and skills.py file path construction

Correctness:
- ISR timezone: replace hardcoded UTC+3 with ZoneInfo("Asia/Jerusalem") for DST
- save_rule: use EspoCrmApiError.status_code instead of fragile string matching
- Background tasks: store references to prevent GC before completion
- Lock race: use dict.setdefault() for atomic lock creation
- Version: health.py now matches main.py (0.2.0)
- Skill names: normalized to lowercase for dedup consistency

Build:
- Dockerfile: install from pyproject.toml instead of hardcoded pip list
- Remove unused mcp>=1.0.0 dependency from pyproject.toml

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 20:18:01 +00:00
chaim a990f527a2 fix: save_rule falls back to user profile when AssistantRule 404
The AssistantRule entity may not exist on some CRM instances. When save_rule
gets a 404, it now falls back to saving the rule in the local user profile
(update_user_profile). Also improved update_user_profile description so
Shira is more likely to use it for user preferences.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 20:10:10 +00:00
chaim 6f7b94d6f8 feat: initial shira-hermes service
Shira AI backend using OpenAI-compatible agent with tool-calling loop,
replacing the smart-assistant route in ai-gateway.

Components:
- FastAPI adapter (same HTTP contract as ai-gateway)
- Agent runner with agentic tool-calling loop via ai-gateway /v1/chat/completions
- EspoCRM REST API client
- 20 MCP tools (CRM, documents, legal assistance)
- Prompt builder ported from ai-gateway JS to Python
- SOUL.md personality definition
- Dockerfile for Coolify deployment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-13 17:29:35 +00:00