Upload 2 files
Browse files- chart_engine.py +70 -0
- pdf_renderer.py +26 -0
chart_engine.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import yaml, json, os
|
| 2 |
+
from crewai import Agent, Task, Crew, Process
|
| 3 |
+
from langchain_openai import ChatOpenAI
|
| 4 |
+
|
| 5 |
+
class PatientChartCrewEngine:
|
| 6 |
+
def __init__(self, folder_path: str, measure_name: str):
|
| 7 |
+
if not measure_name.endswith(".yml"):
|
| 8 |
+
measure_name += ".yml"
|
| 9 |
+
|
| 10 |
+
file_path = os.path.join(folder_path, measure_name)
|
| 11 |
+
if not os.path.exists(file_path):
|
| 12 |
+
raise FileNotFoundError(f"Measure file not found at: {file_path}")
|
| 13 |
+
|
| 14 |
+
with open(file_path, 'r') as f:
|
| 15 |
+
self.cfg = yaml.safe_load(f)
|
| 16 |
+
|
| 17 |
+
self.llm = ChatOpenAI(model="gpt-5.1", temperature=0.5)
|
| 18 |
+
|
| 19 |
+
def run(self):
|
| 20 |
+
scribe = Agent(
|
| 21 |
+
role="Senior Clinical Quality Scribe",
|
| 22 |
+
goal="Generate a dense, 2-page clinical encounter note with mandatory dates for all entries.",
|
| 23 |
+
backstory="""You are a senior scribe specializing in audit-ready records.
|
| 24 |
+
Every diagnosis, procedure, and immunization MUST have a specific date (MM/DD/YYYY).
|
| 25 |
+
You must use all YAML target_rules and expand the physical exam to hit 2 pages.""",
|
| 26 |
+
llm=self.llm,
|
| 27 |
+
allow_delegation=False
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
architect = Agent(
|
| 31 |
+
role="Medical Document Architect",
|
| 32 |
+
goal="Format clinical data into a professional 2-page LaTeX document.",
|
| 33 |
+
backstory="""You are a LaTeX expert. You invent unique clinic branding and
|
| 34 |
+
use high-density formatting (no whitespace) to ensure the chart is 2 pages long.""",
|
| 35 |
+
llm=self.llm,
|
| 36 |
+
allow_delegation=False
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
content_task = Task(
|
| 40 |
+
description=f"""
|
| 41 |
+
MEASURE RULES: {json.dumps(self.cfg.get('target_rules', {}), indent=2)}
|
| 42 |
+
|
| 43 |
+
DIRECTIONS FOR AUDIT VALIDITY:
|
| 44 |
+
1. MANDATORY DATES: Every diagnosis (ICD-10), procedure (CPT/HCPCS), and
|
| 45 |
+
immunization MUST include a specific date of occurrence (MM/DD/YYYY).
|
| 46 |
+
2. NARRATIVE VOLUME: Write a 10-sentence HPI and a 12-point Review of Systems (ROS).
|
| 47 |
+
3. PHYSICAL EXAM: Include a comprehensive multi-system physical exam.
|
| 48 |
+
4. PATIENT: John Thompson (DOB 05/09/1951).
|
| 49 |
+
""",
|
| 50 |
+
agent=scribe,
|
| 51 |
+
expected_output="Extensive clinical JSON with mandatory dates."
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
latex_task = Task(
|
| 55 |
+
description="""
|
| 56 |
+
Convert the output into professional LaTeX.
|
| 57 |
+
- BRANDING: Invent a UNIQUE clinic name/address (Never use ABC Medicine).
|
| 58 |
+
- PAGE FLOW: Ensure the content fills 2 full pages.
|
| 59 |
+
- CLEANLINESS: Output ONLY the LaTeX code. No summaries or conversational text.
|
| 60 |
+
- RETURN ONLY RAW LaTeX code starting with \\documentclass.
|
| 61 |
+
""",
|
| 62 |
+
agent=architect,
|
| 63 |
+
context=[content_task],
|
| 64 |
+
expected_output="Complete LaTeX source code."
|
| 65 |
+
)
|
| 66 |
+
|
| 67 |
+
crew = Crew(agents=[scribe, architect], tasks=[content_task, latex_task], process=Process.sequential)
|
| 68 |
+
|
| 69 |
+
result = crew.kickoff()
|
| 70 |
+
return result.raw if hasattr(result, 'raw') else str(result)
|
pdf_renderer.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os, subprocess, re, shutil
|
| 2 |
+
|
| 3 |
+
def render_pdf(latex_code, output_path):
|
| 4 |
+
# Strip AI conversation and isolate LaTeX block
|
| 5 |
+
match = re.search(r"```(?:latex)?\n?(.*?)\n?```", latex_code, re.DOTALL)
|
| 6 |
+
clean_code = match.group(1).strip() if match else latex_code.strip()
|
| 7 |
+
|
| 8 |
+
tex_filename = "final_output.tex"
|
| 9 |
+
with open(tex_filename, "w") as f:
|
| 10 |
+
f.write(clean_code)
|
| 11 |
+
|
| 12 |
+
try:
|
| 13 |
+
# pdflatex requires 2 passes for stable headers and page numbering
|
| 14 |
+
for _ in range(2):
|
| 15 |
+
subprocess.run(["pdflatex", "-interaction=nonstopmode", tex_filename],
|
| 16 |
+
check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
| 17 |
+
|
| 18 |
+
# Use shutil to move from local Colab disk to Google Drive
|
| 19 |
+
if os.path.exists("final_output.pdf"):
|
| 20 |
+
shutil.move("final_output.pdf", output_path)
|
| 21 |
+
return True
|
| 22 |
+
return False
|
| 23 |
+
|
| 24 |
+
except subprocess.CalledProcessError:
|
| 25 |
+
print("❌ LaTeX Error: The AI produced invalid syntax. Check the logs.")
|
| 26 |
+
return False
|