Compare commits
3 Commits
eadce36742
...
af455cf3d4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
af455cf3d4 | ||
|
|
580415a05a | ||
|
|
82d0e5b5cb |
56
app.py
56
app.py
@@ -77,26 +77,35 @@ def _autofill_values(uid, form_id):
|
|||||||
profile = store.get_profile(uid) if uid else {}
|
profile = store.get_profile(uid) if uid else {}
|
||||||
about = profile.get("about_me", "")
|
about = profile.get("about_me", "")
|
||||||
display = profile.get("display_name", "")
|
display = profile.get("display_name", "")
|
||||||
field_desc = ", ".join(f"{f['key']} ({f.get('label', '')})" for f in fm)
|
# filter to a manageable set: all text fields + checkboxes with a real label (capped)
|
||||||
|
text_fields = [f for f in fm if f.get("type") != "checkbox"]
|
||||||
|
cb_fields = [f for f in fm if f.get("type") == "checkbox" and f.get("label", "").strip()]
|
||||||
|
cb_fields = cb_fields[:40]
|
||||||
|
sel = text_fields + cb_fields
|
||||||
|
field_desc = ", ".join(
|
||||||
|
f"{f['key']} ({f.get('label', '')})" + (" [CB]" if f.get('type') == 'checkbox' else "")
|
||||||
|
for f in sel
|
||||||
|
)
|
||||||
prompt = (
|
prompt = (
|
||||||
"You are filling a Washington State court form. Map the user's facts to the form fields.\n"
|
"You are filling a Washington State court form. Map the user's facts to the form fields.\n"
|
||||||
f"Form: {spec['name']}\n"
|
f"Form: {spec['name']}\n"
|
||||||
f"Fields: {field_desc}\n\n"
|
f"Fields: {field_desc}\n\n"
|
||||||
f"User's display name: {display or '(not set)'}\n"
|
f"User's display name: {display or '(not set)'}\n"
|
||||||
f"User's information (About Me):\n{about or '(not provided)'}\n\n"
|
f"User's information (About Me):\n{about or '(not provided)'}\n\n"
|
||||||
"Output ONLY valid JSON like {\"county\": \"...\", \"petitioner\": \"...\"}. "
|
"Output ONLY valid JSON. For text fields, output the value (or empty string if unknown). "
|
||||||
"For any field you cannot determine, output an empty string \"\". "
|
"For CHECKBOX fields, output 'X' ONLY when the user's facts clearly and unambiguously "
|
||||||
|
"match that option; otherwise output ''. Be conservative — do not guess. "
|
||||||
"Petitioner is the user themself (use the display name); respondent is their spouse."
|
"Petitioner is the user themself (use the display name); respondent is their spouse."
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
resp = _ollama_chat(RAG_MODEL, [{"role": "user", "content": prompt}],
|
resp = _ollama_chat(RAG_MODEL, [{"role": "user", "content": prompt}],
|
||||||
temperature=0.1, num_predict=800)
|
temperature=0.1, num_predict=1600)
|
||||||
content = resp.get("message", {}).get("content", "")
|
content = resp.get("message", {}).get("content", "")
|
||||||
m = re.search(r"\{.*\}", content, re.DOTALL)
|
m = re.search(r"\{.*\}", content, re.DOTALL)
|
||||||
vals = json.loads(m.group(0)) if m else {}
|
vals = json.loads(m.group(0)) if m else {}
|
||||||
except Exception:
|
except Exception:
|
||||||
vals = {}
|
vals = {}
|
||||||
return {f["key"]: str(vals.get(f["key"], "") or "") for f in fm}
|
return {f["key"]: str(vals.get(f["key"], "") or "") for f in sel}
|
||||||
|
|
||||||
|
|
||||||
def _save_pdf_to_vault(uid, path, name):
|
def _save_pdf_to_vault(uid, path, name):
|
||||||
@@ -338,6 +347,13 @@ def _build_answer(agent, message, history, about_me, settings, uid, display_name
|
|||||||
|
|
||||||
final = resp.get("message", {})
|
final = resp.get("message", {})
|
||||||
answer = (final.get("content") or final.get("thinking") or "").strip()
|
answer = (final.get("content") or final.get("thinking") or "").strip()
|
||||||
|
if not answer:
|
||||||
|
# transient empty response — fall back to the reliable model (no tools)
|
||||||
|
try:
|
||||||
|
resp = _ollama_chat(GENERAL_MODEL, messages)
|
||||||
|
answer = (resp.get("message", {}).get("content") or "").strip()
|
||||||
|
except Exception:
|
||||||
|
answer = ""
|
||||||
answer = _strip_sources(answer)
|
answer = _strip_sources(answer)
|
||||||
return {"answer": answer, "citations": citations, "grounded": bool(context_blocks)}
|
return {"answer": answer, "citations": citations, "grounded": bool(context_blocks)}
|
||||||
|
|
||||||
@@ -547,34 +563,10 @@ def api_forms_autofill():
|
|||||||
uid = _uid()
|
uid = _uid()
|
||||||
d = request.get_json(force=True, silent=True) or {}
|
d = request.get_json(force=True, silent=True) or {}
|
||||||
form_id = d.get("form_id", "")
|
form_id = d.get("form_id", "")
|
||||||
spec = forms.FORM_NAMES.get(form_id)
|
if not forms.FORM_NAMES.get(form_id):
|
||||||
fm = forms.FIELDMAP.get(form_id, [])
|
|
||||||
if not spec:
|
|
||||||
return jsonify({"error": "unknown form"}), 400
|
return jsonify({"error": "unknown form"}), 400
|
||||||
profile = store.get_profile(uid) if uid else {}
|
values = _autofill_values(uid, form_id)
|
||||||
about = profile.get("about_me", "")
|
return jsonify({"form_id": form_id, "values": values})
|
||||||
display = profile.get("display_name", "")
|
|
||||||
field_desc = ", ".join(f"{f['key']} ({f.get('label','')})" for f in fm)
|
|
||||||
prompt = (
|
|
||||||
"You are filling a Washington State court form. Map the user's facts to the form fields.\n"
|
|
||||||
f"Form: {spec['name']}\n"
|
|
||||||
f"Fields: {field_desc}\n\n"
|
|
||||||
f"User's display name: {display or '(not set)'}\n"
|
|
||||||
f"User's information (About Me):\n{about or '(not provided)'}\n\n"
|
|
||||||
"Output ONLY valid JSON like {\"county\": \"...\", \"petitioner\": \"...\"}. "
|
|
||||||
"For any field you cannot determine, output an empty string. "
|
|
||||||
"Petitioner is the user themself (use the display name); respondent is their spouse."
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
resp = _ollama_chat(RAG_MODEL, [{"role": "user", "content": prompt}],
|
|
||||||
temperature=0.1, num_predict=800)
|
|
||||||
content = resp.get("message", {}).get("content", "")
|
|
||||||
m = re.search(r"\{.*\}", content, re.DOTALL)
|
|
||||||
vals = json.loads(m.group(0)) if m else {}
|
|
||||||
except Exception:
|
|
||||||
vals = {}
|
|
||||||
known = {f["key"]: str(vals.get(f["key"], "") or "") for f in fm}
|
|
||||||
return jsonify({"form_id": form_id, "values": known})
|
|
||||||
|
|
||||||
|
|
||||||
@app.route("/api/forms/fill", methods=["POST"])
|
@app.route("/api/forms/fill", methods=["POST"])
|
||||||
|
|||||||
@@ -85,6 +85,30 @@ def detect_underscores(pno, page):
|
|||||||
return fields
|
return fields
|
||||||
|
|
||||||
|
|
||||||
|
def detect_checkboxes(pno, page):
|
||||||
|
"""Find checkbox glyphs (U+F0A8) and their labels. Type 'checkbox'."""
|
||||||
|
fields = []
|
||||||
|
words = page.get_text("words")
|
||||||
|
groups = {}
|
||||||
|
for w in words:
|
||||||
|
x0, y0, x1, y1, word = w[0], w[1], w[2], w[3], w[4]
|
||||||
|
key = int(y0 / 5) * 5
|
||||||
|
groups.setdefault(key, []).append((x0, x1, y0, word))
|
||||||
|
for y in sorted(groups):
|
||||||
|
ws = sorted(groups[y], key=lambda t: t[0])
|
||||||
|
for i, (x0, x1, y0, word) in enumerate(ws):
|
||||||
|
if "\uf0a8" in word:
|
||||||
|
label_words = []
|
||||||
|
for wx0, wx1, _, w in ws[i + 1:]:
|
||||||
|
if "\uf0a8" in w:
|
||||||
|
break
|
||||||
|
label_words.append(w)
|
||||||
|
label = " ".join(label_words).strip()
|
||||||
|
fields.append({"page": pno, "x": x0, "y": y0, "type": "checkbox",
|
||||||
|
"label": label})
|
||||||
|
return fields
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
all_fields = {}
|
all_fields = {}
|
||||||
for fn in sorted(os.listdir(FORMS_DIR)):
|
for fn in sorted(os.listdir(FORMS_DIR)):
|
||||||
@@ -99,9 +123,9 @@ def main():
|
|||||||
continue
|
continue
|
||||||
fields = []
|
fields = []
|
||||||
seen_keys = set()
|
seen_keys = set()
|
||||||
for pno in range(min(len(d), 3)): # caption + first 2 pages
|
for pno in range(min(len(d), 12)): # caption + all pages
|
||||||
lines = lines_with_pos(d[pno])
|
|
||||||
if pno == 0:
|
if pno == 0:
|
||||||
|
lines = lines_with_pos(d[pno])
|
||||||
for f in detect_caption(lines):
|
for f in detect_caption(lines):
|
||||||
f["page"] = 0
|
f["page"] = 0
|
||||||
if f["key"] not in seen_keys:
|
if f["key"] not in seen_keys:
|
||||||
@@ -110,6 +134,9 @@ def main():
|
|||||||
for f in detect_underscores(pno, d[pno]):
|
for f in detect_underscores(pno, d[pno]):
|
||||||
f["key"] = "f_" + str(len(fields))
|
f["key"] = "f_" + str(len(fields))
|
||||||
fields.append(f)
|
fields.append(f)
|
||||||
|
for f in detect_checkboxes(pno, d[pno]):
|
||||||
|
f["key"] = "cb_" + str(len(fields))
|
||||||
|
fields.append(f)
|
||||||
d.close()
|
d.close()
|
||||||
all_fields[fid] = fields
|
all_fields[fid] = fields
|
||||||
out = "/opt/astraea/forms/fieldmap.json"
|
out = "/opt/astraea/forms/fieldmap.json"
|
||||||
|
|||||||
14
forms.py
14
forms.py
@@ -51,7 +51,8 @@ def list_forms():
|
|||||||
out = []
|
out = []
|
||||||
for fid, spec in FORM_NAMES.items():
|
for fid, spec in FORM_NAMES.items():
|
||||||
fm = FIELDMAP.get(fid, [])
|
fm = FIELDMAP.get(fid, [])
|
||||||
fields = [{"key": f["key"], "label": f.get("label", f["key"])} for f in fm]
|
fields = [{"key": f["key"], "label": f.get("label", f["key"]),
|
||||||
|
"type": f.get("type", "text")} for f in fm]
|
||||||
out.append({"id": fid, "name": spec["name"], "description": spec["description"],
|
out.append({"id": fid, "name": spec["name"], "description": spec["description"],
|
||||||
"fields": fields})
|
"fields": fields})
|
||||||
return out
|
return out
|
||||||
@@ -72,10 +73,19 @@ def fill_form(form_id, values):
|
|||||||
f = fmap.get(key)
|
f = fmap.get(key)
|
||||||
if not f or not text:
|
if not f or not text:
|
||||||
continue
|
continue
|
||||||
|
page = doc[f.get("page", 0)]
|
||||||
|
if f.get("type") == "checkbox":
|
||||||
|
v = str(text).strip().lower()
|
||||||
|
if v in ("x", "true", "yes", "1", "checked", "check", "on"):
|
||||||
|
x, y = f["x"], f["y"]
|
||||||
|
page.draw_line(pymupdf.Point(x + 1, y + 1), pymupdf.Point(x + 9, y + 9),
|
||||||
|
color=(0, 0, 0), width=1.3)
|
||||||
|
page.draw_line(pymupdf.Point(x + 9, y + 1), pymupdf.Point(x + 1, y + 9),
|
||||||
|
color=(0, 0, 0), width=1.3)
|
||||||
|
else:
|
||||||
text = str(text).strip()
|
text = str(text).strip()
|
||||||
if not text:
|
if not text:
|
||||||
continue
|
continue
|
||||||
page = doc[f.get("page", 0)]
|
|
||||||
page.insert_text((f["x"], f["y"]), text, fontsize=f.get("size", 11),
|
page.insert_text((f["x"], f["y"]), text, fontsize=f.get("size", 11),
|
||||||
fontname="helv", color=(0, 0, 0))
|
fontname="helv", color=(0, 0, 0))
|
||||||
out = os.path.join("/tmp", f"filled_{form_id}_{uuid.uuid4().hex[:8]}.pdf")
|
out = os.path.join("/tmp", f"filled_{form_id}_{uuid.uuid4().hex[:8]}.pdf")
|
||||||
|
|||||||
@@ -344,19 +344,25 @@
|
|||||||
}catch(e){ pv.textContent='Upload failed'; pv.style.color='#f87171'; }
|
}catch(e){ pv.textContent='Upload failed'; pv.style.color='#f87171'; }
|
||||||
}
|
}
|
||||||
const INTAKE=[
|
const INTAKE=[
|
||||||
{label:"My name", q:"What's your full name?", p:"Jane Marie Doe"},
|
{label:"My full legal name", q:"What's your full legal name (first, middle, last)?", p:"Jane Marie Doe"},
|
||||||
{label:"Spouse's name", q:"What's your spouse's full name?", p:"John Robert Doe"},
|
{label:"My mailing address", q:"What's your current mailing address (street, city, state, ZIP)?", p:"123 Main St, Everett, WA 98201"},
|
||||||
{label:"Married", q:"When and where did you get married?", p:"June 15, 2011, Seattle, WA"},
|
{label:"Spouse's full legal name", q:"What's your spouse's full legal name?", p:"John Robert Doe"},
|
||||||
{label:"Separated", q:"When did you separate?", p:"June 2025"},
|
{label:"Spouse's mailing address", q:"What's your spouse's mailing address?", p:"456 Oak Ave, Everett, WA 98201"},
|
||||||
{label:"Children", q:"How many children do you share, and their names and ages?", p:"Two — Emma (12) and Liam (9)"},
|
{label:"Marriage date & place", q:"When and where did you get married (exact date + city/county/state)?", p:"June 15, 2011, Seattle, WA"},
|
||||||
|
{label:"Separation date", q:"When did you separate (exact date)?", p:"June 1, 2025"},
|
||||||
|
{label:"Case number", q:"Has a case been filed? What's the case number (if you know it)?", p:"Not yet / 24-3-01234-31"},
|
||||||
|
{label:"Children (names, ages, DOBs)", q:"List each child's full name, age, and date of birth.", p:"Emma Grace Doe, 12, born 01/15/2013; Liam James Doe, 9, born 03/22/2016"},
|
||||||
{label:"Current custody", q:"Where do the children live now, and what's the current schedule?", p:"They live with me; dad has them every other weekend"},
|
{label:"Current custody", q:"Where do the children live now, and what's the current schedule?", p:"They live with me; dad has them every other weekend"},
|
||||||
{label:"Home", q:"Do you own a home or other real estate? Address, when bought, whose name is on the title/mortgage?", p:"House at 123 Main St, bought 2012, both names on title"},
|
{label:"Home / real estate", q:"Do you own a home or other real estate? Address, when bought, whose name is on the title/mortgage?", p:"House at 123 Main St, bought 2012, both names on title"},
|
||||||
{label:"Assets", q:"What are your main assets? (bank accounts, retirement, vehicles, business, investments)", p:"Joint savings ~$20k, my 401k ~$85k, two cars"},
|
{label:"Assets", q:"What are your main assets? (bank accounts, retirement, vehicles, business, investments)", p:"Joint savings ~$20k, my 401k ~$85k, two cars"},
|
||||||
{label:"Debts", q:"What are your main debts? (mortgage, credit cards, loans)", p:"Mortgage $280k remaining, $9k credit card, $12k car loan"},
|
{label:"Debts", q:"What are your main debts? (mortgage, credit cards, loans)", p:"Mortgage $280k remaining, $9k credit card, $12k car loan"},
|
||||||
{label:"Income", q:"What's your employment and income? And your spouse's?", p:"I make $72k/yr as a nurse; spouse makes $95k as an engineer"},
|
{label:"My income & employer", q:"What's your job, employer, and annual gross income?", p:"Registered nurse, Providence, $72k/yr"},
|
||||||
{label:"Case status", q:"Has either of you filed for divorce yet, and what does each side want?", p:"Spouse filed; wants full custody and to keep the house"},
|
{label:"Spouse's income & employer", q:"What's your spouse's job, employer, and annual gross income?", p:"Software engineer, Amazon, $95k/yr"},
|
||||||
{label:"Safety", q:"Is there any domestic violence, abuse, or safety concern? Any protection orders?", p:"No / Yes — details"},
|
{label:"Children's health insurance", q:"Who provides health insurance for the children, and through which employer?", p:"My employer (Providence) covers the kids"},
|
||||||
{label:"Goals", q:"What outcome matters most to you? What are you most worried about?", p:"Keeping the kids with me and keeping the house"},
|
{label:"Case status", q:"Has either of you filed for divorce yet, and what does each side want (custody, support, property)?", p:"Spouse filed; wants full custody and to keep the house"},
|
||||||
|
{label:"Safety / DV", q:"Is there any domestic violence, abuse, or safety concern? Any protection orders in place?", p:"No / Yes — details"},
|
||||||
|
{label:"Other cases", q:"Are there any other court cases involving you, your spouse, or the children (criminal, CPS, other family law)?", p:"None"},
|
||||||
|
{label:"Goals & worries", q:"What outcome matters most to you? What are you most worried about?", p:"Keeping the kids with me and keeping the house"},
|
||||||
];
|
];
|
||||||
let intakeIdx=0; const intakeAnswers={};
|
let intakeIdx=0; const intakeAnswers={};
|
||||||
function openIntake(){ intakeIdx=0; for(const k in intakeAnswers) delete intakeAnswers[k]; closeModal('profileModal'); renderIntake(); document.getElementById('intakeModal').classList.add('open'); }
|
function openIntake(){ intakeIdx=0; for(const k in intakeAnswers) delete intakeAnswers[k]; closeModal('profileModal'); renderIntake(); document.getElementById('intakeModal').classList.add('open'); }
|
||||||
|
|||||||
Reference in New Issue
Block a user