pharmascan-api / serving /app /tools.py
DavidPanSX's picture
deploy API
414ecc0 verified
Raw History Blame Contribute Delete
6.95 kB
"""
Typed tool definitions for the Medication Care Agent (blueprint job B14), generated from the API's own OpenAPI
document so they can't drift from it. Served at GET /v1/tools; usage notes and example traces in docs/agent_tools.md.
Each tool: name (the blueprint's where it names one), description written for the agent (when to call it, what it
must not conclude), input_schema (JSON Schema; an uploaded file becomes an image reference the app's tool layer
resolves), output_schema ($refs into `components`), and how to call it (method, path, content type).
"""
TOOLS = [
("route_medication_image", "post", "/v1/check-input",
"Before any other photo tool when you don't know what the photo shows: loose pills, blister pack, box or bottle, "
"printed label or document, and which tools fit (next_tools). Prefer asking the user; when type is unchecked or "
"next_tools is empty, ask for the box, label or prescription, or suggest the pharmacist."),
("detect_pills", "post", "/v1/detect",
"Find each pill in a photo and say whether the count can be trusted. Call before counting pills or embedding "
"several pills. If count_reliable is false, don't count: ask for a new photo (pills apart, plain surface)."),
("embed_pill", "post", "/v1/embed",
"Turn one loose-pill photo (or a photo + box) into a 256-number embedding. Use at enrolment (store it with "
"model_version and embedding_space_id) and before match_gallery. Never embed boxes or blister packs."),
("match_gallery", "post", "/v1/match",
"Compare pill embeddings with the patient's gallery: match / unknown / ambiguous per pill. Visual evidence only: "
"a match means 'looks like', not 'is' or 'safe to take'. ambiguous: ask for another photo. The gallery's "
"model_version is required; on 409 re-embed the enrolment photos."),
("identify_pill", "post", "/v1/identify",
"Possible catalogue matches (US products) for a pill that isn't in the patient's list. Report the verdict and "
"confidence, never a definite identity; no_confident_match means the pill may not be a catalogue product."),
("search_by_embedding", "post", "/v1/search-by-embedding",
"identify_pill for a pill you already embedded (e.g. match_gallery said unknown): send its embeddings and "
"imprint_reads from embed_pill. Same answer, no second model run. Possible matches only, never an identity."),
("scan_photo", "post", "/v1/scan",
"Diagnostics in one call: detection plus each pill's visual decision (with a gallery) or catalogue candidates. "
"Don't use its dose check (deprecated): apply the backend's dose rules to the pills it returns."),
("extract_medication_document", "post", "/v1/read-label",
"Read a Singapore pharmacy sticker (or a prescription or box; photo or PDF): per medicine the name, strength, "
"RxNorm code, quantity, the instructions in parts (dose, times a day or every N hours, as needed, time of day, "
"before/after food), what it is for, cautions and dates, each with its source line. Parts not printed are "
"null; never fill them in. With the patient's medications, says same medicine / different strength / not on "
"the list. Always have the patient confirm before anything is scheduled."),
("find_medication_product", "get", "/v1/pills/search",
"Products and pictures for a medicine name (brand or generic), with strength, dose form and release as fields. "
"Never treat products of another strength or form as the same."),
("screen_medication_list", "post", "/v1/safety/medications",
"Interaction and duplicate-ingredient alerts for the whole medication list (DDInter 2.0). Read coverage: no "
"alert does not mean safe. Advisory; defer to the pharmacist."),
("evaluate_dose_reference", "post", "/v1/safety/dose",
"The reference dose rules on visual decisions (stateless). The backend's own dose policy is authoritative."),
("retrieve_medication_info", "get", "/v1/safety/label",
"Warning sections of the official US label, with the label's date and DailyMed link for citation. General "
"information, not the patient's instructions."),
("get_model_info", "get", "/v1/model-info",
"The served model: version, embedding space, decision rule and known limits. Check after a 409."),
]
FILE_NOTE = "An image reference: the app's tool layer uploads the file (photo, or PDF for documents)"
def _resolve(schema, components):
ref = schema.get("$ref", "") if isinstance(schema, dict) else ""
return components.get(ref.rsplit("/", 1)[-1], {}) if ref else schema
def _input_schema(op, components):
props, required = {}, []
for p in op.get("parameters", []):
props[p["name"]] = {**p.get("schema", {}), "description": p.get("description", "")}
if p.get("required"):
required.append(p["name"])
content = op.get("requestBody", {}).get("content", {})
for kind in ("application/json", "multipart/form-data"):
if kind in content:
body = _resolve(content[kind]["schema"], components)
for name, s in body.get("properties", {}).items():
is_file = s.get("format") == "binary" or s.get("contentMediaType") or \
(s.get("type") == "array" and (s.get("items", {}).get("format") == "binary"
or s.get("items", {}).get("contentMediaType")))
props[name] = ({"type": "array", "items": {"type": "string"}, "description": FILE_NOTE}
if is_file and s.get("type") == "array" else
{"type": "string", "description": FILE_NOTE} if is_file else s)
required += body.get("required", [])
return {"type": "object", "properties": props, "required": sorted(set(required))}
def build_tools(openapi):
components = openapi.get("components", {}).get("schemas", {})
tools = []
for name, method, path, description in TOOLS:
op = openapi["paths"][path][method]
ok = op.get("responses", {}).get("200", {}).get("content", {}).get("application/json", {})
kind = next(iter(op.get("requestBody", {}).get("content", {"none": None})))
tools.append({"name": name, "description": description, "input_schema": _input_schema(op, components),
"output_schema": ok.get("schema", {}),
"call": {"method": method.upper(), "path": path,
"content_type": None if kind == "none" else kind}})
return {"tools": tools, "components": {"schemas": components},
"notes": "Stage 2 answers are visual evidence only. The backend's safety policy decides actions; the "
"agent reports evidence with its limits and asks the patient to confirm."}