Spaces:
Running on Zero
Running on Zero
Download serving/app/tools.py from DavidPanSX/pharmascan-api: direct link, hf CLI and curl.
- Browser
- Download file 6.95 kB
-
https://huggingface.co/spaces/DavidPanSX/pharmascan-api/resolve/main/serving/app/tools.py
- Command line
-
hf download hf://spaces/DavidPanSX/pharmascan-api/serving/app/tools.py
-
curl -L -o tools.py https://huggingface.co/spaces/DavidPanSX/pharmascan-api/resolve/main/serving/app/tools.py
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."} | |