KevinIsInCoding
perf: rank trials by phase, cap at 15, strip bloat, tighten defaults (#25)
a009989 unverified
Raw
History Blame Contribute Delete
9.35 kB
from __future__ import annotations
import datetime
import json
from dataclasses import asdict
from typing import Generator
import anthropic
from rich.panel import Panel
from rich.text import Text
from beacon_logging import get_logger
from config import INTAKE_MODEL
from llm import cached_system, cached_tools
from models import PatientProfile, geocode_zip
from prompts import INTAKE_SYSTEM, lookup_disease_profile
from tools import INTAKE_TOOLS
from translations import LANGUAGE_DIRECTIVE
from _console import console
_logger = get_logger("agents.intake")
def _months_from_date(date_str: str) -> int:
"""Convert YYYY-MM string to elapsed months from today. Returns 0 on parse failure."""
try:
year, month = int(date_str[:4]), int(date_str[5:7])
today = datetime.date.today()
return (today.year - year) * 12 + (today.month - month)
except (ValueError, IndexError):
return 0
def _resolve_months(data: dict, date_key: str, months_key: str) -> int:
"""Prefer date string for exact computation; fall back to LLM-supplied integer."""
if data.get(date_key):
return _months_from_date(data[date_key])
return data.get(months_key) or 0
def run_intake_agent(client: anthropic.Anthropic) -> PatientProfile:
today = datetime.date.today().strftime("%B %d, %Y")
console.print()
console.print(Panel(
Text("Beacon β€” Rare Disease Clinical Trial Finder", justify="center", style="bold cyan"),
border_style="cyan",
padding=(1, 4),
))
messages: list[anthropic.types.MessageParam] = [
{"role": "user", "content": "Please begin."}
]
while True:
response = client.messages.create(
model=INTAKE_MODEL,
max_tokens=1024,
system=cached_system(f"Today's date is {today}.\n\n" + INTAKE_SYSTEM),
tools=cached_tools(INTAKE_TOOLS),
messages=messages,
)
text = next((b.text for b in response.content if b.type == "text"), "")
if text:
console.print(f"\n[bold cyan]Beacon:[/bold cyan] {text}")
identify_block = next(
(b for b in response.content if b.type == "tool_use" and b.name == "identify_disease"),
None,
)
submit_block = next(
(b for b in response.content if b.type == "tool_use" and b.name == "submit_profile"),
None,
)
if identify_block:
disease = lookup_disease_profile(identify_block.input["standardized_name"])
tool_result = json.dumps({
"benchmarks_to_collect": disease["benchmarks"] if disease else [],
"message": (
f"Collect these benchmarks for {disease['full_name']}"
if disease else "Disease not in registry β€” skip benchmark questions."
),
})
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": [{
"type": "tool_result",
"tool_use_id": identify_block.id,
"content": tool_result,
}]})
continue
if submit_block:
data = submit_block.input
try:
with console.status("[cyan]Geocoding location…[/cyan]", spinner="dots"):
lat, lon = geocode_zip(data["zip_code"], data.get("country_code", "US"))
except Exception as exc:
console.print(f"[yellow]Warning:[/yellow] Geocoding failed ({exc}) β€” coordinates set to 0,0.")
lat, lon = 0.0, 0.0
profile = PatientProfile(
disease=data["disease"],
age=data["age"],
onset_months=_resolve_months(data, "onset_date", "onset_months"),
diagnosis_months=_resolve_months(data, "diagnosis_date", "diagnosis_months"),
benchmarks=data.get("benchmarks") or {},
zip_code=data["zip_code"],
country_code=data.get("country_code", "US"),
lat=lat,
lon=lon,
radius_miles=data.get("radius_miles", 20),
phases=data.get("phases") or [],
include_eap=data.get("include_eap", False),
include_observational=data.get("include_observational", False),
)
_logger.info(
"Patient intake complete (CLI)",
extra={"data": {"intake_summary": asdict(profile)}},
)
return profile
messages.append({"role": "assistant", "content": response.content})
user_input = input("\nYou: ").strip() or "(no response)"
messages.append({"role": "user", "content": user_input})
def intake_greeting(client: anthropic.Anthropic, lang: str = "en") -> tuple[str, list]:
"""Run the opening intake turn (blocking). Returns (greeting_text, initial_messages)."""
today = datetime.date.today().strftime("%B %d, %Y")
system = f"Today's date is {today}.\n\n" + LANGUAGE_DIRECTIVE[lang] + INTAKE_SYSTEM
messages: list[anthropic.types.MessageParam] = [{"role": "user", "content": "Please begin."}]
response = client.messages.create(
model=INTAKE_MODEL,
max_tokens=1024,
system=cached_system(system),
tools=cached_tools(INTAKE_TOOLS),
messages=messages,
)
text = next((b.text for b in response.content if b.type == "text"), "")
return text, messages + [{"role": "assistant", "content": response.content}]
def stream_intake_turn(
client: anthropic.Anthropic,
messages: list[anthropic.types.MessageParam],
lang: str = "en",
) -> Generator[tuple, None, None]:
"""
Stream one user turn of the intake conversation.
Yields:
("token", str) β€” partial text chunk
("reset_stream",) β€” identify_disease handled; clear token buffer
("text", str, list) β€” model responded with text; updated messages
("profile", PatientProfile, list) β€” profile submitted; updated messages
"""
today = datetime.date.today().strftime("%B %d, %Y")
system = f"Today's date is {today}.\n\n" + LANGUAGE_DIRECTIVE[lang] + INTAKE_SYSTEM
new_msgs = list(messages)
while True:
intake_text = ""
with client.messages.stream(
model=INTAKE_MODEL,
max_tokens=1024,
system=cached_system(system),
tools=cached_tools(INTAKE_TOOLS),
messages=new_msgs,
) as stream:
for chunk in stream.text_stream:
intake_text += chunk
yield ("token", chunk)
response = stream.get_final_message()
intake_text = intake_text or next(
(b.text for b in response.content if b.type == "text"), ""
)
identify_block = next(
(b for b in response.content if b.type == "tool_use" and b.name == "identify_disease"),
None,
)
submit_block = next(
(b for b in response.content if b.type == "tool_use" and b.name == "submit_profile"),
None,
)
if identify_block:
disease = lookup_disease_profile(identify_block.input["standardized_name"])
tool_result = json.dumps({
"benchmarks_to_collect": disease["benchmarks"] if disease else [],
"message": (
f"Collect these benchmarks for {disease['full_name']}"
if disease else "Disease not in registry β€” skip benchmark questions."
),
})
new_msgs = new_msgs + [
{"role": "assistant", "content": response.content},
{"role": "user", "content": [{
"type": "tool_result",
"tool_use_id": identify_block.id,
"content": tool_result,
}]},
]
yield ("reset_stream",)
continue
new_msgs = new_msgs + [{"role": "assistant", "content": response.content}]
if submit_block:
data = submit_block.input
try:
lat, lon = geocode_zip(data["zip_code"], data.get("country_code", "US"))
except Exception:
lat, lon = 0.0, 0.0
profile = PatientProfile(
disease=data["disease"],
age=data["age"],
onset_months=_resolve_months(data, "onset_date", "onset_months"),
diagnosis_months=_resolve_months(data, "diagnosis_date", "diagnosis_months"),
benchmarks=data.get("benchmarks") or {},
zip_code=data["zip_code"],
country_code=data.get("country_code", "US"),
lat=lat,
lon=lon,
radius_miles=data.get("radius_miles", 20),
phases=data.get("phases") or [],
include_eap=data.get("include_eap", False),
include_observational=data.get("include_observational", False),
lang=lang,
)
yield ("profile", profile, new_msgs)
return
yield ("text", intake_text, new_msgs)
return