From b12f97ba25b29a111e4186b68e6dcdc843ffdfc6 Mon Sep 17 00:00:00 2001 From: tcaden3575 Date: Sat, 13 Dec 2025 22:54:07 -0500 Subject: [PATCH] Annotation Test --- .env | 1 + .env.example | 1 + .github/workflows/ci.yml | 15 ++ .idea/.gitignore | 3 + .idea/agentic-gemini-ai-react.iml | 14 ++ .idea/googol.iml | 8 + .idea/inspectionProfiles/Project_Default.xml | 65 ++++++ .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 4 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + docs/diagrams/archetecture.txt | 21 ++ eval/__init__.py | 0 eval/eval_runner.py | 21 ++ eval/evaluate_rag.py | 16 ++ pyproject.toml | 53 +++++ requirements.txt | Bin 0 -> 5828 bytes src/agent/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 167 bytes src/agent/__pycache__/agent.cpython-312.pyc | Bin 0 -> 1128 bytes .../__pycache__/executor.cpython-312.pyc | Bin 0 -> 1686 bytes src/agent/__pycache__/planner.cpython-312.pyc | Bin 0 -> 879 bytes src/agent/agent.py | 14 ++ src/agent/coordinator.py | 9 + src/agent/executor.py | 28 +++ src/agent/planner.py | 42 ++++ src/annotate_main.py | 15 ++ src/core/__init__.py | 0 src/core/__pycache__/__init__.cpython-312.pyc | Bin 0 -> 166 bytes src/core/__pycache__/executor.cpython-312.pyc | Bin 0 -> 1758 bytes src/core/__pycache__/memory.cpython-312.pyc | Bin 0 -> 1873 bytes src/core/__pycache__/planner.cpython-312.pyc | Bin 0 -> 632 bytes src/core/executor.py | 36 ++++ src/core/memory.py | 20 ++ src/core/planner.py | 7 + src/dicom_main.py | 29 +++ src/llm/__init__.py | 0 src/llm/gemini_client | 201 ++++++++++++++++++ src/main.py | 17 ++ src/memory/__init__.py | 0 src/memory/vector_store.py | 82 +++++++ src/pipeline/__init__.py | 0 src/pipeline/async_ingest.py | 9 + src/tools/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 167 bytes src/tools/__pycache__/base.cpython-312.pyc | Bin 0 -> 709 bytes .../dicom_download_tool.cpython-312.pyc | Bin 0 -> 3911 bytes .../dicom_ontology_presenter.cpython-312.pyc | Bin 0 -> 2487 bytes .../__pycache__/download_tool.cpython-312.pyc | Bin 0 -> 1700 bytes .../__pycache__/gemini_client.cpython-312.pyc | Bin 0 -> 1034 bytes .../__pycache__/metadata_tool.cpython-312.pyc | Bin 0 -> 1039 bytes .../__pycache__/search_tool.cpython-312.pyc | Bin 0 -> 621 bytes .../__pycache__/vector_tool.cpython-312.pyc | Bin 0 -> 713 bytes src/tools/base.py | 11 + src/tools/dicom_download_tool.py | 88 ++++++++ src/tools/dicom_ontology_llm_tool.py | 19 ++ src/tools/dicom_ontology_presenter.py | 58 +++++ src/tools/dicom_vector_index_tool.py | 20 ++ src/tools/download_tool.py | 33 +++ src/tools/gemini_client.py | 10 + src/tools/image_to_dicom.py | 133 ++++++++++++ src/tools/metadata_tool.py | 17 ++ src/tools/search_tool.py | 6 + src/tools/test.py | 1 + src/tools/vector_tool.py | 15 ++ src/utils/__init__.py | 0 .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 167 bytes src/utils/__pycache__/config.cpython-312.pyc | Bin 0 -> 404 bytes src/utils/__pycache__/logger.cpython-312.pyc | Bin 0 -> 437 bytes src/utils/config.py | 6 + src/utils/logger.py | 3 + src/utils/metadata.py | 149 +++++++++++++ tests/__init__.py | 0 tests/test_basic.py | 4 + tests/test_ontology_explanation.py | 15 ++ tests/test_vector_index.py | 19 ++ 76 files changed, 1358 insertions(+) create mode 100644 .env create mode 100644 .env.example create mode 100644 .github/workflows/ci.yml create mode 100644 .idea/.gitignore create mode 100644 .idea/agentic-gemini-ai-react.iml create mode 100644 .idea/googol.iml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 docs/diagrams/archetecture.txt create mode 100644 eval/__init__.py create mode 100644 eval/eval_runner.py create mode 100644 eval/evaluate_rag.py create mode 100644 pyproject.toml create mode 100644 requirements.txt create mode 100644 src/agent/__init__.py create mode 100644 src/agent/__pycache__/__init__.cpython-312.pyc create mode 100644 src/agent/__pycache__/agent.cpython-312.pyc create mode 100644 src/agent/__pycache__/executor.cpython-312.pyc create mode 100644 src/agent/__pycache__/planner.cpython-312.pyc create mode 100644 src/agent/agent.py create mode 100644 src/agent/coordinator.py create mode 100644 src/agent/executor.py create mode 100644 src/agent/planner.py create mode 100644 src/annotate_main.py create mode 100644 src/core/__init__.py create mode 100644 src/core/__pycache__/__init__.cpython-312.pyc create mode 100644 src/core/__pycache__/executor.cpython-312.pyc create mode 100644 src/core/__pycache__/memory.cpython-312.pyc create mode 100644 src/core/__pycache__/planner.cpython-312.pyc create mode 100644 src/core/executor.py create mode 100644 src/core/memory.py create mode 100644 src/core/planner.py create mode 100644 src/dicom_main.py create mode 100644 src/llm/__init__.py create mode 100644 src/llm/gemini_client create mode 100644 src/main.py create mode 100644 src/memory/__init__.py create mode 100644 src/memory/vector_store.py create mode 100644 src/pipeline/__init__.py create mode 100644 src/pipeline/async_ingest.py create mode 100644 src/tools/__init__.py create mode 100644 src/tools/__pycache__/__init__.cpython-312.pyc create mode 100644 src/tools/__pycache__/base.cpython-312.pyc create mode 100644 src/tools/__pycache__/dicom_download_tool.cpython-312.pyc create mode 100644 src/tools/__pycache__/dicom_ontology_presenter.cpython-312.pyc create mode 100644 src/tools/__pycache__/download_tool.cpython-312.pyc create mode 100644 src/tools/__pycache__/gemini_client.cpython-312.pyc create mode 100644 src/tools/__pycache__/metadata_tool.cpython-312.pyc create mode 100644 src/tools/__pycache__/search_tool.cpython-312.pyc create mode 100644 src/tools/__pycache__/vector_tool.cpython-312.pyc create mode 100644 src/tools/base.py create mode 100644 src/tools/dicom_download_tool.py create mode 100644 src/tools/dicom_ontology_llm_tool.py create mode 100644 src/tools/dicom_ontology_presenter.py create mode 100644 src/tools/dicom_vector_index_tool.py create mode 100644 src/tools/download_tool.py create mode 100644 src/tools/gemini_client.py create mode 100644 src/tools/image_to_dicom.py create mode 100644 src/tools/metadata_tool.py create mode 100644 src/tools/search_tool.py create mode 100644 src/tools/test.py create mode 100644 src/tools/vector_tool.py create mode 100644 src/utils/__init__.py create mode 100644 src/utils/__pycache__/__init__.cpython-312.pyc create mode 100644 src/utils/__pycache__/config.cpython-312.pyc create mode 100644 src/utils/__pycache__/logger.cpython-312.pyc create mode 100644 src/utils/config.py create mode 100644 src/utils/logger.py create mode 100644 src/utils/metadata.py create mode 100644 tests/__init__.py create mode 100644 tests/test_basic.py create mode 100644 tests/test_ontology_explanation.py create mode 100644 tests/test_vector_index.py diff --git a/.env b/.env new file mode 100644 index 000000000..5325bb430 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +GEMINI_API_KEY=Enter Key diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..5325bb430 --- /dev/null +++ b/.env.example @@ -0,0 +1 @@ +GEMINI_API_KEY=Enter Key diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..19fee3818 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,15 @@ +# .github/workflows/ci.yml +name: CI + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - run: pip install .[dev] + - run: pytest diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 000000000..26d33521a --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# Default ignored files +/shelf/ +/workspace.xml diff --git a/.idea/agentic-gemini-ai-react.iml b/.idea/agentic-gemini-ai-react.iml new file mode 100644 index 000000000..8e5446ac9 --- /dev/null +++ b/.idea/agentic-gemini-ai-react.iml @@ -0,0 +1,14 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/googol.iml b/.idea/googol.iml new file mode 100644 index 000000000..d0876a78d --- /dev/null +++ b/.idea/googol.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 000000000..f19474821 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,65 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 000000000..105ce2da2 --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 000000000..d1e22ecb8 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 000000000..c325f830f --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 000000000..94a25f7f4 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/docs/diagrams/archetecture.txt b/docs/diagrams/archetecture.txt new file mode 100644 index 000000000..252d95c45 --- /dev/null +++ b/docs/diagrams/archetecture.txt @@ -0,0 +1,21 @@ + ┌──────────────┐ + │ TCIA API │ + └──────┬───────┘ + │ + Async Batch Ingestion + │ + ┌──────▼───────┐ + │ DICOM FS │ ← src/data/ + └──────┬───────┘ + │ + Metadata Extraction (no pixels) + │ + ┌──────▼───────┐ + │ Vector DB │ ← FAISS / Chroma + └──────┬───────┘ + │ + Retrieval-Augmented Context + │ + ┌──────▼───────┐ + │ Gemini LLM │ ← ontology reasoning + └──────────────┘ diff --git a/eval/__init__.py b/eval/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/eval/eval_runner.py b/eval/eval_runner.py new file mode 100644 index 000000000..efb8523ad --- /dev/null +++ b/eval/eval_runner.py @@ -0,0 +1,21 @@ +from typing import List + + +class EvalCase: + def __init__(self, prompt, expected_keywords: List[str]): + self.prompt = prompt + self.expected_keywords = expected_keywords + + + class Evaluator: + def run(self, agent, cases: List[EvalCase]): + results = [] + for case in cases: + response = agent(case.prompt) + score = sum(1 for k in case.expected_keywords if k in response.lower()) + results.append({ + "prompt": case.prompt, + "score": score, + "response": response + }) + return results \ No newline at end of file diff --git a/eval/evaluate_rag.py b/eval/evaluate_rag.py new file mode 100644 index 000000000..6ea9edf4c --- /dev/null +++ b/eval/evaluate_rag.py @@ -0,0 +1,16 @@ +# eval/evaluate_rag.py + +import asyncio + +QUERIES = [ + ("lung CT", "CHEST"), + ("brain MRI", "HEAD") +] + +async def evaluate(store): + score = 0 + for query, expected in QUERIES: + results = await store.similarity_search(query) + if expected in str(results[0].metadata): + score += 1 + return score / len(QUERIES) diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 000000000..d57baf169 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,53 @@ +[build-system] +requires = ["setuptools>=68", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "dicom-agent-framework" +version = "0.1.0" +description = "Agentic AI framework for DICOM ingestion, vector search, ontology reasoning, and RAG" +readme = "README.md" +requires-python = ">=3.10" +license = { text = "Apache-2.0" } + +authors = [ + { name = "Your Name", email = "you@email.com" } +] + +keywords = [ + "DICOM", + "Healthcare AI", + "Medical Imaging", + "RAG", + "Vector Database", + "Ontology", + "Agentic AI" +] + +dependencies = [ + "pydicom>=2.4.4", + "numpy>=1.24", + "faiss-cpu>=1.7.4", + "sentence-transformers>=2.6.0", + "google-generativeai>=0.5.0", + "nbiatoolkit>=1.0.0", + "asyncio", + "tqdm" +] + +[project.optional-dependencies] +dev = [ + "pytest>=8.0", + "pytest-asyncio>=0.23", + "ruff", + "black", + "mypy" +] + +[project.urls] +Homepage = "https://github.com/YOUR_ORG/dicom-agent-framework" +Repository = "https://github.com/YOUR_ORG/dicom-agent-framework" +Issues = "https://github.com/YOUR_ORG/dicom-agent-framework/issues" + +[tool.setuptools.packages.find] +where = ["src"] diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..01eecee8097b1568335d766d8b927c4c0555cc99 GIT binary patch literal 5828 zcmaKwPjgyV48`xdGyN!J3dZ@f=pw7mbefrTGF^1#!4NPG1_C&C{NdZ4pUx9{*rb!O zO^~jxbR-?=y?_6iv{9S1LEE=k`_g{ZzhBy`cA=k98@8X@Pm-?MLBE%67xGKV`1z%i zuG_k8LgUar%gR8XzqhSE?~nH0oald-txd?r`u<%{w%6H%WM6c8-JQo5d`fT7*mbf| zcw!BFz1F!&yO+kY&GmOBy^a2E+xU3jP@jp=`YUO!<32|>l&n0uj>tCB1{q{y;qt1z z?M@T-rcL#;)6M5~_TAmYz|SboPUYvc-RKk+HsLAfUrU2~jP6wOT^G$V?2jaI)L=?1 z#4%O0b47v${PSIw+~_9o;-*35YS-@c9g#nUeNKTPxTJSTr(H$7jqfu9M7xTJCt-)v z+{fwHoi`gvPOHG!VZ(Wx$|tIX=ok7t(dQd|gEeD}P2)V1m&SM!`c;R-YI`&JOZDLQ zby!KCjO0xBP@n7e?I9wQTUX7x2%j(RCDd)-JrUGtd_v(2p{yYA{5 z`$(D1wtg#oiQMOzHmP$Vl^ZVzB<`d4%x_d8o{iK&`2SMtl#x=Oe*2L=F+U1l`$=WP7M0Dk;SR_f zYi4C$LBZ4br4C+!8xvuxoDff+@193p*3BQEmF0V#ViW7%cN$gy6r3`T%AD#9%uxsu z(J9LPlc$>?74_y>Tzlmolw40(BX6G7AC#?~qGad#q|bBtfyY@-_L%xHBg2ellR==u zXQop|SsZ;Qi$2${PxRw6&vM~XQ2L*m>HY8Lz_L;nN8mJ4p;tA5pWf}P3GK>WX}wYmA=lE z^vIz**s+1D``+5-!K@ReZQ_V6&+EJ6ZFuZn_^7-3lehfAM%|DxRi)d5+}iMhihR(| zTG)H`j`X+Uh9%gfxC86XRN_Xx!>EIZ9bG>;IIBdRXJC8mWS!Th!P(Pi5>Pe1b9Cg#uyC$ZtvTN_D7s`cO-hA8q@+D$;O#nLHyphe1|ou zt(Wt~?1PtRu}z!zYR;R%j5a!7g`e?uhq-R}?aV*d;T?l{mnZ(5irkBz#+sdYCiLiC zvw?G1d;r7L9#eI=SERgSpz`-I!Mqc3zP~SXn)#L!g}S%@Xx98;Yg&q#nGl2MM|#fl z7@up`v}XEi9P!sku`Sb!&J7!J-%B%R?JZ`YM8nVjOyxw*p1uvlT&-k`x#(>9_I?yR z)9{o-EuQicjKp@we&-p%X}Zo;mv3RP4*wN9cx(97{!~4_7dE+HI%8A-SdNc&c(!56 zW#>76==5;{BW|I?sGancKCjXAhwU%86o7k6`?s8qzRb~^cmEUB~Z^!cOO;sEWmWZQAWJKk<<+pnm*)EVq z%9*Nt%h-Ux4mo#|?TqHDBxYxjc-=$G)ku_nlC@_(i>|Bq|7sSk%dnWbW@=c-`(bVw z&v#l*riXRr#~YvZ`=_217dwb6s<_IS{S_6SZ=(2H^^*0#geQ8QkJuNmWLTnQb4OWT@2KDgWARP;ybI(V4fy7pEIdVr^JdF)nts#5%(6$E;e@C7xf2h% zjc(`J&HP6tm=*ByA>yN7sT6ONuG7ThK{du4n#t>WR&b5l(?@X)&v*MyPOTRXPx_vG PcrV?_PF6FMI&b+8$@^Dv literal 0 HcmV?d00001 diff --git a/src/agent/__init__.py b/src/agent/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/agent/__pycache__/__init__.cpython-312.pyc b/src/agent/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c95070104bfde10b901da71e9660dc64b0a2c8e4 GIT binary patch literal 167 zcmX@j%ge<81O-oRGePuY5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!a&xwd2`x@7Dvl{h zPE1LSamg>w%gIknDUL}@Pt7aIOx8_L&CSfq)J@FPElN#HE{Q2FN`}bA#K!|AO5)@7 j3Mzkb*yQG?l;)(`6|n-1Wd!145aS~=BO_xGGmr%U?FuUl literal 0 HcmV?d00001 diff --git a/src/agent/__pycache__/agent.cpython-312.pyc b/src/agent/__pycache__/agent.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1700cbb8fd34ea28cc094a92bc9f2c9b2a6a2a46 GIT binary patch literal 1128 zcmZuvO=uHQ5T3VxNt0|>5H(0Ep(v6=dhsSz6e}L8MG!rN;Iiz#mXzI1eY>HiY@ml8 zQcx^Z2;#wOOD=lRt9bQd>7iw5PoBI)WmR zX9DmO4@1)ipg+Sw7d-HY4*{tXf>`mDK&`3*tG*WKRh@tW55Uvb!P9$+tgad@qn!DL zhcZbsk9{W$DdRAE|0Q*saRYJT8Fe{&M1uxvxw24t-jlF?k^gmpLX~(>RXnl|Rn=3t z*IH^>OZB^pG>qLL9c!=wmH+zBx$8na%JujeEdhHJi**?TIFrR{Ve!}yoym?WTChXr z%LYSwnpu_;GE^_k(!r1n*{KnT3HiiYME!*buP>e4b8pt3MwCUh*mXQwn{BLwe#7yi znu9~t-O3^j>S4X&)GLfSZd{9)D|6CMFSpXHZF5R&+pFV3is|9m;CDom+^xakr7K&l zj`pf>SS-EIabO-^oZ1RI+UtCu6Tlt38DhYOg?w*0=yI`^F#3-zjU+lvB&0WxwXN`~ z;TD||U@3FvFn|eiv;T>c-r$ZJ@2;)3wM0#{1?34ae3Y`zaDmgj%aw_#jq0;JMlw=* z$_kQZVlda_I&-0xn{lckH#M9vTA?h}W9GOt!l_GWY#jM2Ylf?1Qcw=@!_gh?*5JT8 z|0dZ;_O0u?*7fb&o^_+69TX=%nD5N}Vr92j+4erpewq6;_x18#@!^KqF*fxB>%wNU z)Bcg2VCVQp=F2)hTj&JTw$q$#2Mw?3BQDtXi>Bkt46`_w;r{P{s1vO*)KEBVIl>{= z;aE}iYyF(6Pf8+k=F6G1;5HbY9&V=CQ&WUS>nXN1FJf{>#lp%-zp*G2uepVbp%)cXk%pZ7`PLae*p?~@`(Td literal 0 HcmV?d00001 diff --git a/src/agent/__pycache__/executor.cpython-312.pyc b/src/agent/__pycache__/executor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f953725126a9e9fa1744cfe5ab8233e4ad658b8a GIT binary patch literal 1686 zcmZux&2Jk;6o30=ZyZNSWG8V#nz#@`eTYTj76n8h6j6m-T4;z2t7Uf@HeK84%sOqN zwZtI@M+!87gIx&;S3)AFm;N7Ih*A`5)I%i>aI|71q@H-Q>rHUgcX;3PcHX|v3H0nTLRkC4K zwk4w6l5KfMT~%!*&{fnmTMP6U>aneOPUiz3-Xm7UE7L1$q~vkUE0b3m@jg z$S-crhLC*&t&xeEx|e8;jn~v~lO6N{S53Os+pdQ0pxwVwse=Pm*_YPOS`?>3deoJR z@I?WjfQ?m^s_Kce0r{#I4HTjQ()WFJJsxG8OB`|c3-mH(^qof{&6aFAs#4W_SPx(n zMMJqh&}$3J*P_b4>?;fC*<10|GyO{yeEB-$l;!Qj%myW!PPxMJ9CC54IUXr8o^Y5` zVxC#D2v?X#c>)dUvAd4S*cIWBf zc;n)a*PjetY?{Bg58cNLPiEh1jgHlw1E-lh{OWl0^1jlVdi5vwfqOEQKbp!Pvg4_@ z_w~l`_x;bNrbDIOj|JOF7|PTJsrb=c#WqVlYa!YQaMD7h4!&edq zCD{8MG4Rrphzo6*Ys@1XjH}{h2*&y=x`Sn=;+^6Ot(3y~$0{4P>5*JKgn(!eS07LF zK5^J6nMFb;(Ho*8hQqD69usp70g(+65f3+)JUq3a&Te8!L=4$_&%*uf{Kp!Z#> zEw(a~jn5t=Yah2#WA*ui`G#|pLie-O*hy;UC^gf(aX9<1aGZJzQ%5fB->9z~tQ?KJ z-c*s*uC`+^FnoV$cj+WKeUzMT#{NjY(H_Ra?KBLf@B6#{@A@kN8kTW8@Zk~d7zR%m zMzL&HT%jio<4(nNyAjoPERTw14*nE3YO7%Db&l_CV%M0%2K_phtN;^GF$$X zT&@~&hFtU#QFMx^^VKa+dg#P$dt?@G_iN9Zc{xLXdJaWBmpb547yLt?dZ&IbIPu$o zuDwcdnu6FJK&rGq;5gA00V)?MnO2yjt{61Eq02{;oYbK%^{7vSPcE&P=N7HfIp$1w zNN>|x$p;>D4p6&3+6|-W+T-aF2o>ST7K>P_xZzTXoS3RWBRB?dZD;{ue*f zz93snLUJhVSfstvD7t#Fx6^+uLCC%$m_q;g^qXWK+d(k0HGm;Y*%oG7rX*2)DM)$N zx3^*@*2zw^Z~18H<_tSlvXhbr20oyohU$&++U9tD=ljFWi@<9&FM`U_a dict: + """ + Convert a goal into a structured execution plan + """ + # In production, this is an LLM call + return { + "steps": [ + { + "tool": "download_files", + "args": { + "url": "https://example.com/dicom", + "output_dir": "data" + } + }, + { + "tool": "extract_metadata", + "args": { + "files": "$previous.files" + } + }, + { + "tool": "create_vector_store", + "args": { + "metadata": "$previous.metadata" + } + }, + { + "tool": "image_to_dicom", + "args": { + "image_path": "annotated_output.png", + "output_dcm": "annotated_output.dcm", + "patient_name": "DOE^JANE", + "patient_id": "A12345", + "study_description": "AI Annotated Image", + "modality": "OT" + } + } + + ] + } diff --git a/src/annotate_main.py b/src/annotate_main.py new file mode 100644 index 000000000..bb1019fbc --- /dev/null +++ b/src/annotate_main.py @@ -0,0 +1,15 @@ +# main.py +from tools.image_to_dicom_tool import ImageToDICOMTool + + +def main(): + tool = ImageToDICOMTool() + + result = tool.run( + image_path="input.jpg", + output_dcm="output.dcm", + study_description="AI Generated Image" + ) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/core/__init__.py b/src/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/core/__pycache__/__init__.cpython-312.pyc b/src/core/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a0f43aec43f9268b0d03619d65c8fcef9736b96e GIT binary patch literal 166 zcmX@j%ge<81O-oRGePuY5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!a&@+f2`x@7Dvl{h zPE1LSamg>w%gIknDUL}@Pt7aIOx8_L&CSfq)J@FPElN#HE{Q2FN{&g+FG`Jxj|YmE m#K-FuRQ}?y$<0qG%}KQ@Vg;JY2*kx8#z$sGM#ds$APWGxbSk9) literal 0 HcmV?d00001 diff --git a/src/core/__pycache__/executor.cpython-312.pyc b/src/core/__pycache__/executor.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..52062cb7788ccd3d018274595e2a2fdff6635ab7 GIT binary patch literal 1758 zcmZ`)&2Jl35P$pW^=1=0NeRwJiIqZ9x5T}H6qPnrQx!M_XiKG9Myu`atFvakYu>KY z#@f&zBH>Cwr0T(-Qkr9`T>3X~<&r2xu~w~w#HpOD1PQ4p=Iwf&sDdYX-pst2H@}&E zGybi&S4OZ>zkXo;%pvqA72?Hrn1i#xG?9S}#zGaQz%Z0&Ew;iHxRB>8z9JL^2C?We zGWb%7AU{O<@i9lbO-A-&mns3K^-XwNDCdz0%;eHpL5Yw7DO|YhWMVIJB-66P$J9*@*OM#@SP%TeY>-}+h`B)`F zAom!M74&>?{K2^fza~8&%093f{P#TvphtTA0jZFHUs#D>0AUlA&@%6{b{G^7(IYnYV-RRLsx|m8PXbD2?SzlYN6~by zD0eGY*Frsb%(pp$EVMaTB= z9dAai_sa4WthtVD+NBx!Wq6k&%b(S-8(t;XGqU_CaTZMjXK$IdW@ROLi97qry6V1apnb7(MeAUwIK&;@3LzV)$ z1{uHuuhzUE1ziwX4` zdb-;*9Sya1{{pgtS|ekPRBIs9Jahld#;M(b=|T{y)%4z^W)v&_t(W%dVDWE zxs#sUJpOd?>B9EZ`Df|bR(hzZ+*f|Q{(JiEb`L~o_i;l^TSRPfCD}%t%=`_!uyWRq*qz}@a&k1yv*jfdvY9Om>#W89YL*k z8kZ^+q5-PZJQF;k(NAcIDBvXJDEJV>E9q>6`5ZX~Iy#|T`j61&7>0R)-hF{izCh#u N@Z-$XJ^~VY{tHk`me~LR literal 0 HcmV?d00001 diff --git a/src/core/__pycache__/memory.cpython-312.pyc b/src/core/__pycache__/memory.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d4bf4a65963c0e3be5c9a46278471c66c2a27e7e GIT binary patch literal 1873 zcma)7&2Jk;6rb4-cWWn3^RYAuK`|8yS&AcQ1g=7eR8fHtNiGEr#%Nh~Ct=;Soz869 zIC7*&M8cMe0tZMrAbd$a2ofXt&6_vxWBh)< znfW> zz-hIp7!ls{A+h}eT_CvvE!sFt1GC)vHJ5@+etXZscUpQ0nQg1DhOya5sC8yX>WT>) zftC?uXvYQ66b5m{4AxO={e|!2EJ>5hPhEuz*-WvL0^t;jOrSG4s4X6HQ0*=o|#xQU) zpkXBM7ktYL2ucTFlSde_sQnpNvIRE}ZUHIPYpaA7IlfAt5kA=Dvg@od@s@o83=KJ| zg@-%#2!v+AGG>8*(=}p!+cTA!z1UbSHde(CV$U}Q^`rdNtEcMmN#Jk4zBBsu=wAQ1 zTK~D~^e(RVzf_5oae2DYH@ID_6zjvOd-AG<*}D2oA$b4$o8G z$`RA#QPYHHa@7-hk7-_8wYK*O^vN97PTNq4qkfb>Qzx;VP(Bo1o2V=VVIew!E~}aldv`?5B(qZ z;y)n%13i27v^SZP;6G5zLD-XTk}l|jeD7tx?|t)__kB1F0psA?i|hdb_+@{t!`m0l z7>f&V-~>2qfRnFI$IM~vOk8%Yw;4U!XzpOT00DEtVeW9Ufw^ndCU~1uDTQKtlUD8{ zy7)B@+cr+P!`;sg_qgBY9uI6z-FGnQ&+5DHwNMe&9|VtRbXEynW`#5Xl{8=1V&a&f zTozTjEKQFx%hmVZ&~+pQHi(a+T3Kwl_If6^=SV%hyNVf+w>!ZPw;d6c>DBn@ao6-$@X=dr`!9NgQH#G z-41u)-3S}h!EEB0ev(MK5=mmhBv}=_%56PJlCz5D9iwicR@wcl0}De7b_*@oP3iz) r1C8VRVW+Y5Y(hHO|E*X3=iNOo)d*E>zxCa@gpfZ@kA%Mgq4oI-5(J_X literal 0 HcmV?d00001 diff --git a/src/core/executor.py b/src/core/executor.py new file mode 100644 index 000000000..1f7b37080 --- /dev/null +++ b/src/core/executor.py @@ -0,0 +1,36 @@ +from tools.gemini_client import GeminiClient +from tools.search_tool import SearchTool +from utils.logger import logger + +class Executor: + def __init__(self): + self.llm = GeminiClient() + self.search = SearchTool() + + def execute(self, plan, memory, user_input): + reasoning_trace = [] + context = memory.retrieve(user_input) + + for step in plan: + reasoning_trace.append(f"Thought: {step['thought']}") + if step["action"] == "search": + result = self.search.search(user_input) + reasoning_trace.append(f"Observation: {result}") + memory.store(result) + + prompt = f"""You are a ReAct-style agent. + +Context: +{context} + +Reasoning: +{chr(10).join(reasoning_trace)} + +Question: +{user_input} + +Provide final answer. +""" + answer = self.llm.generate(prompt) + memory.store(answer) + return answer, reasoning_trace diff --git a/src/core/memory.py b/src/core/memory.py new file mode 100644 index 000000000..6ddfd61c5 --- /dev/null +++ b/src/core/memory.py @@ -0,0 +1,20 @@ +import faiss +from sentence_transformers import SentenceTransformer + +class VectorMemory: + def __init__(self): + self.model = SentenceTransformer("all-MiniLM-L6-v2") + self.index = faiss.IndexFlatL2(384) + self.texts = [] + + def store(self, text): + emb = self.model.encode([text]) + self.index.add(emb) + self.texts.append(text) + + def retrieve(self, query, k=3): + if self.index.ntotal == 0: + return "" + emb = self.model.encode([query]) + _, idx = self.index.search(emb, k) + return "\n".join(self.texts[i] for i in idx[0]) diff --git a/src/core/planner.py b/src/core/planner.py new file mode 100644 index 000000000..15e81d897 --- /dev/null +++ b/src/core/planner.py @@ -0,0 +1,7 @@ +class Planner: + def create_plan(self, goal): + return [ + {"thought": "User asked a question", "action": "analyze"}, + {"thought": "May need external info", "action": "search"}, + {"thought": "Answer using Gemini", "action": "final"} + ] diff --git a/src/dicom_main.py b/src/dicom_main.py new file mode 100644 index 000000000..8c1c4e1f2 --- /dev/null +++ b/src/dicom_main.py @@ -0,0 +1,29 @@ +# main.py +from agent.planner import PlannerAgent +from agent.executor import ExecutorAgent +from tools.download_tool import DownloadTool +from tools.metadata_tool import MetadataTool +from tools.vector_tool import VectorStoreTool + +def main(): + tools = { + "download_files": DownloadTool(), + "extract_metadata": MetadataTool(), + "create_vector_store": VectorStoreTool(), + } + + planner = PlannerAgent() + executor = ExecutorAgent(tools) + + goal = "Download DICOM files, extract metadata, and build a vector database" + + plan = planner.plan(goal) + result = executor.execute(plan) + + if result: + #print(result) + print(f"Final vector store contains {len(result.get('vector_store', []))} entries.") + + +if __name__ == "__main__": + main() diff --git a/src/llm/__init__.py b/src/llm/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/llm/gemini_client b/src/llm/gemini_client new file mode 100644 index 000000000..e512798fc --- /dev/null +++ b/src/llm/gemini_client @@ -0,0 +1,201 @@ +""" +Gemini LLM client wrapper for agentic AI systems. + +Supports: +- Planning prompts +- Reasoning / execution prompts +- JSON-safe outputs +- Tool-aware prompting +""" + +from typing import Optional, Dict, Any +import json +import os + +try: + import google.generativeai as genai +except ImportError: + genai = None + + +class GeminiClient: + """ + Lightweight Gemini client for agent planners and reasoners. + """ + + def __init__( + self, + model: str = "gemini-1.5-pro", + api_key: Optional[str] = None, + temperature: float = 0.2, + max_output_tokens: int = 2048 + ): + if genai is None: + raise ImportError( + "google-generativeai is not installed. " + "Run: pip install google-generativeai" + ) + + self.api_key = api_key or os.getenv("GEMINI_API_KEY") + if not self.api_key: + raise ValueError("GEMINI_API_KEY is not set") + + genai.configure(api_key=self.api_key) + + self.model_name = model + self.temperature = temperature + self.max_output_tokens = max_output_tokens + + self.model = genai.GenerativeModel( + model_name=self.model_name, + generation_config={ + "temperature": self.temperature, + "max_output_tokens": self.max_output_tokens, + }, + ) + + # ========================================================== + # Core Generation + # ========================================================== + + def generate( + self, + prompt: str, + system_prompt: Optional[str] = None + ) -> str: + """ + Generate raw text output from Gemini. + """ + full_prompt = prompt + if system_prompt: + full_prompt = f"{system_prompt}\n\n{prompt}" + + response = self.model.generate_content(full_prompt) + return response.text.strip() + + # ========================================================== + # Planner Helper + # ========================================================== + + def generate_plan( + self, + goal: str, + tools_schema: Optional[Dict[str, Any]] = None + ) -> Dict[str, Any]: + """ + Generate a structured execution plan in JSON. + """ + + system_prompt = ( + "You are a planner agent. " + "Break the user's goal into ordered steps. " + "Each step must call exactly one tool. " + "Output ONLY valid JSON." + ) + + tools_description = "" + if tools_schema: + tools_description = ( + "\nAvailable tools:\n" + + json.dumps(tools_schema, indent=2) + ) + + user_prompt = f""" +Goal: +{goal} + +Create a JSON plan with this structure: + +{{ + "steps": [ + {{ + "tool": "", + "args": {{ ... }} + }} + ] +}} +{tools_description} +""" + + text = self.generate(user_prompt, system_prompt) + return self._safe_json_parse(text) + + # ========================================================== + # Reasoning Helper + # ========================================================== + + def reason( + self, + context: str, + question: str + ) -> str: + """ + Use Gemini for reasoning over retrieved context. + """ + + system_prompt = ( + "You are a reasoning agent. " + "Use the provided context to answer the question. " + "If the answer is not present, say so." + ) + + prompt = f""" +Context: +{context} + +Question: +{question} +""" + + return self.generate(prompt, system_prompt) + + # ========================================================== + # Tool-Calling Helper + # ========================================================== + + def tool_decision( + self, + state: Dict[str, Any], + tools_schema: Dict[str, Any] + ) -> Dict[str, Any]: + """ + Decide the next tool call based on current agent state. + """ + + system_prompt = ( + "You are an execution agent. " + "Based on the current state, decide the next tool call. " + "Return ONLY valid JSON." + ) + + prompt = f""" +Current state: +{json.dumps(state, indent=2)} + +Available tools: +{json.dumps(tools_schema, indent=2)} + +Return JSON: +{{ + "tool": "", + "args": {{ ... }} +}} +""" + + text = self.generate(prompt, system_prompt) + return self._safe_json_parse(text) + + # ========================================================== + # Utilities + # ========================================================== + + def _safe_json_parse(self, text: str) -> Dict[str, Any]: + """ + Safely parse JSON from LLM output. + """ + try: + return json.loads(text) + except json.JSONDecodeError: + raise ValueError( + f"Gemini returned invalid JSON:\n{text}" + ) diff --git a/src/main.py b/src/main.py new file mode 100644 index 000000000..333c39e99 --- /dev/null +++ b/src/main.py @@ -0,0 +1,17 @@ +from agent.agent import Agent + +def main(): + agent = Agent() + print("Gemini ReAct Agent (type exit to quit)") + while True: + q = input("User: ") + if q.lower() in ("exit", "quit"): + break + answer, trace = agent.run(q) + print("\n--- ReAct Trace ---") + for t in trace: + print(t) + print("\nAgent:", answer) + +if __name__ == "__main__": + main() diff --git a/src/memory/__init__.py b/src/memory/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/memory/vector_store.py b/src/memory/vector_store.py new file mode 100644 index 000000000..91c45ba2c --- /dev/null +++ b/src/memory/vector_store.py @@ -0,0 +1,82 @@ +""" +Vector store tool for agentic AI pipelines. + +Wraps memory.vector_store.VectorStore so it can be: +- Planned +- Executed +- Evaluated +- Swapped between backends +""" + +from typing import List, Dict, Any, Optional +from tools.base import Tool +from memory.vector_store import VectorStore + + +class VectorStoreTool(Tool): + """ + Agent-facing vector memory tool. + """ + + name = "vector_store" + description = ( + "Store text embeddings in vector memory and perform similarity search. " + "Supports add_texts and similarity_search actions." + ) + + def __init__(self, embedding_fn): + self.store = VectorStore(embedding_fn) + + # ============================================================== + # Tool Interface + # ============================================================== + + def run(self, action: str, **kwargs) -> Dict[str, Any]: + """ + Entry point for agent execution. + + action: + - add_texts + - similarity_search + - count + - clear + """ + + if action == "add_texts": + return self._add_texts(**kwargs) + + if action == "similarity_search": + return self._similarity_search(**kwargs) + + if action == "count": + return {"count": self.store.count()} + + if action == "clear": + self.store.clear() + return {"status": "cleared"} + + raise ValueError(f"Unsupported vector_store action: {action}") + + # ============================================================== + # Actions + # ============================================================== + + def _add_texts( + self, + texts: List[str], + metadatas: Optional[List[Dict[str, Any]]] = None + ) -> Dict[str, Any]: + return self.store.add_texts( + texts=texts, + metadatas=metadatas + ) + + def _similarity_search( + self, + query: str, + k: int = 5 + ) -> Dict[str, Any]: + return self.store.similarity_search( + query=query, + k=k + ) diff --git a/src/pipeline/__init__.py b/src/pipeline/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/pipeline/async_ingest.py b/src/pipeline/async_ingest.py new file mode 100644 index 000000000..d0050b407 --- /dev/null +++ b/src/pipeline/async_ingest.py @@ -0,0 +1,9 @@ +# src/dicom_agent/pipeline/async_ingest.py +import asyncio +from tools.dicom_download_tool import download_dicom_from_tcia +from tools.dicom_vector_index_tool import index_dicom_to_vector_db + + +async def ingest_collection(collection: str, store): + dicoms = await asyncio.to_thread(download_dicom_from_tcia, collection) + await index_dicom_to_vector_db(dicoms, store) \ No newline at end of file diff --git a/src/tools/__init__.py b/src/tools/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/tools/__pycache__/__init__.cpython-312.pyc b/src/tools/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4caddb107000a80d538da1f274067f169a95f778 GIT binary patch literal 167 zcmX@j%ge<81O-oRGePuY5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!a&xwd2`x@7Dvl{h zPE1LSamg>w%gIknDUL}@Pt7aIOx8_L&CSfq)J@FPElN#HE{Q2FN{%VX&(A52iH`?L nl*GsD6;%G>u*uC&Da}c>D`Ev2%Lv59AjU^#Mn=XWW*`dy^S&!C literal 0 HcmV?d00001 diff --git a/src/tools/__pycache__/base.cpython-312.pyc b/src/tools/__pycache__/base.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3b52838ba2b0b3605c1f39b2ed55b6a3246f8fec GIT binary patch literal 709 zcmY*Wzi$&U6n;MYuGh2<w}zhDuBsl7+1*NKGmMOD4LUPO_Y{6HeTZVmqqBfRHE> z|3P5u2B?1uBTMCl3M3F)A+U79&Lto`eDC}G?C-s2e~6+6*!A}7o$RxXUz}Jje?ey3 zmP~;IM>!NIMuiu9$Z0Rf1&K-F$36lN?g7U~z=WuU%q2EZTC!9ZLO-f_THmViZ~3m(QxFg%4zafn0t>>?j@_LzZ_a#S>r;bGCR;&RhmEwJ(gvP7U!2?&5{|hFog-UQ2+gW z3MKNP2_7FZIa12A*{!XMZf`r;S3;^pr;LkauR1LAigA^&ktlVRwnw7K%B;<@wiI^Y zM9DPKwjY%Ym=c|XiNSJQJ`G)u9m`g&v2t(l9L~b^nI+I~t(uTh*FUAEL1|I(aqjp! zrBB8zUu3X-Q*)V8R+g1!ZbC(=46K86|03N)a>GI7;HrZw4#Fk4x~gO0C7d>*7Y|N1 znjcB?%-=lskRQF)$Gvx($M@en`w3RgFa*sXE>T-Kmws!-1bT9im7{+(4Q7K>I`6uf rdAa{vxCnRZq3+5X_MH0=bT}vY_Qe*7utwPmsd?zW@v~C8dFr`ik4*^e zcKgyxeD3c#_s%`v`OeK>8yW-zCHDNR`WRY&Cmp+3Gtk+SL98Mf$y5?eliHD_QjTc{ zMcU4!Geu9+(59yu9`Q(Slo^GUS@^jWm&_>+g>455vz*L-%uI8#@G+X^Wj9Dc_JDND z4In)^D(L-^>>UkKCOx6*2Gm0Iy+|mWRF$-`6z;w(8Ke~%?eD&7WaV5Wts7E0p>Qqpk+Ew&lBqh zN*+C6>{ZK5o|)$kEPyUkbK(&9!gL%Kq^2F|4uQH>ZOc-O^QEt{4>VzCb2VG$F6 zff`?ni>22<%l@32PSs2_=rJ9dZgMGUL4l9bF}Xz3J*OB|1G>p5sf>{`JyY4Vp{A5E zj5Tc1YEny>JgmEF-Bf);**sDivLg*5(QLGDq`EpVI(8v+Z6X%G5SbW@Uk=4$V^h%} zk4ZpamIwlW(`TzGa-3YDZn6n0@J-IX2h&w;nSA9VL57}rlPeRS#P!dmPJ{l->ndyx)2F+N2yB{7c`nxy$BL%MP_J4ZrlXFj-ju$(_kKTVYQtY_A(R8Kgxw6e7 z|7)wgcY4>TQqzfoYb$VcGtgBEbgeIL_Mb2HpDzZwHUgmnUv>vRefN`hiHVMpji$3j z&)Kc!&M($TBgL_*PZvO!L~PzhlY48vkH$YR3SCJjMw z)1>D$HEl8(3}j_CLu|$&c~t@@uIcebDY;-elS(?sm<^+oKa5UHhDJ&7T{MrxLQ~^o zv3PiLVqz>Di%dpM&pudV(_O{36@=8RPP}DB6c)+;jpS3~wadZ1FxUq_{R0sHK>q~r zdr+Wdb@0yM>d2juwT2CU-^#>OnlD7}j@<1l3c<(p8_$|vU-xV@4Xj*(iK3_HxBB{- zjn@Z@Jwrv|oyYX>Rzuq^q8ML+DmQw6=Gt*W`)i#b?N;bJq2pAdmVjZ13|q0qteSvXcM;M#iAW?1OMM*?{p{IW%QE@{FB`Ai=WpHK^9q;0~Q~@2V zl*vHno(tI6PG9iAff?TN9!Xv%jWx|6-mc^~K&8o4n3p3}8gdjY2P9gWIteig2BK&I z0r!xNi8u#8Jq2O~m3>Vsk*8ffzaRh2_&z}oeEqp3gjMvGt*iwzmqAvbSy1+gb81=9hfl2H0H20- z9R~PK=$dRc11}8)e;+!pxXx^UnJ`jP#wHC+oOzKbVG#eup+g59I`r)v`rY+NvGeUG zzCq&EA>!2`+pG8tZ2O8=3C8#S45FG|YSDjKda0)ph~;qAs}zZEi};5K@&6CeP37rY zbdv}9H%B*9O)kUWw=qaCh$NVb=NOC25Az&2^)2{N{5BDTM8JU&eus!*5Ecnm;;_QU z9WVQ`O8)x@`aEa>xRL%N5Eeln3{df)KgIvbpDi7~@DdM-JK#a_YwMM8E94J#P=9LS zK?WU5oc)acl4Ox!LnVc=@ZeR`a~NnB9-!$PDr8wLxu6=>lN6Kxp_oqHz@`(joAtD^ zvP^|>2te_95ciM;7V8#Xa3d*A&L)*}7@`*5An>_A2C?m=C~A*Il<);={Q{lXYMm*f zz&3|kJIgJtJFZSjDEs`|PN=t_W}t?L){($AnSo`jP&oO~^*tVXyYEvCly6t$sOH@O Mh`j(yiPj1J1x6l;Gynhq literal 0 HcmV?d00001 diff --git a/src/tools/__pycache__/dicom_ontology_presenter.cpython-312.pyc b/src/tools/__pycache__/dicom_ontology_presenter.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2183f9540aeb31bc646fef61cabd522065229cd5 GIT binary patch literal 2487 zcmb_eUu+ab7@ytSy+3SZU+3BJ(l>ZRT(ol%0Bq1(7;Fy|Agbh#UF zf>mUuJ%jW5j7_;|;AxXugyEi%v%>(|u47`(m|VB&y08gAnl2~#EXBqwWgLSkY!&Sp zmM(V)u0BPaylKM;L%(eDQo7tO*t1k8K6c90O*>sa1g60TX7v3cwG9B6GqVmF2i&a%^}_pAaohIkw?2ZBz!)$MSe+~dt%&e>@(=y7KA2bgsw_g zwu<(v#lvq{~cP2xI*4{9xb2IYwPJ!3~|ziH!4s zZ899?LsRVp%1g3dZ4Mjiycr~z!FIm4tEU4%IbOQwT@BOq%1?3;hF_* zI(9jBlG?yD@EBBPN_BUF7hI8$?kJ*+x!VE7gwL?sE)FE< z6^Nz5C-VRyI@jI3z!qBB+c3?eKRP-W&o1p=rfVIqtZ${0yB7zRwB@0-U6h0$%Iw?Zuhl!O;;sEZQy}x>B%o8&c1)F~~Hn&d!wzv`@P~{~h+zq$qrZ@6UgU zi*cbv>xUywzSMy10bWrgil&Jy!#Qfq+9ovNhP+G2+v;+W)mWr;LneM$albEXQ~(}? z9s*fb2MmiAB3l1&rVsnsTb|Uk+^oqkd6edOv%fHCe8ReDf#PNHU!q2;ViTH^He1Z= zw&-3&pR(Y|v7ny`v{5&k)>5h$DwO<5_U(Rw8DMc4JN#a;K5Um_>w2iBudA8XOB`Akhw z5^61sg4(r1t7=C@*}gz;4KI=9zSV=r);eFU?0&6=0>@S8h5sAm#Z`5CMcDy*cea7v z_8ki!uC{eoTKCtGazLu>O~#aedXTDpGW6-mS}Li={OU|;?g98QUXYgx;B;3Mq^pQY zq>L8;uhsn*g6PBF2VQrn@WdVDYw#WSF#JZ|Aw`S6>83#>3X&CIss$uT+6W>k{u}Cq zTSZ^4YKN=ZzG_Q%HGZ%f+gsI+J&1?f<@tCm8I+DL4Au}Vi-Q|tsU^ZvaB;`CU0-+A j5Rl8cmF^X`(sSqHhS=7UJEhq2SPj9lGWNGv{2ToT)kk}) literal 0 HcmV?d00001 diff --git a/src/tools/__pycache__/download_tool.cpython-312.pyc b/src/tools/__pycache__/download_tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..bcab8983885adc5400d966131d3965427b936930 GIT binary patch literal 1700 zcmah}&1)M+6rcT)CE1Rp%8u=%P33+B7A!Y{QsR)7C{l2ZjZ|#41)(xbc1O;pE3G=S zB1kCUg9|$Jkeey=&|`x8N90)OrJ^wsd+DL=p*J;356!7>cD2STgbwU`Z{ECl^Y-_C zGe6DG#}SOL|M}4RRz>JHF$sq@F~%Q&ag0o4N;axX6-g4dY|C|}qSV!jDj^wdAyYX* zrb?v~CD>Qd+L|8uQ%rQpQ_GHH>$10EIu9DQLriR1hEvBi2EsKg;=Or5T}*5n*DRZI z>~Q>m*k9rU+MtZM4l{zZfrMK9F}?=s7*SM_OjMCgsiK&2MHRJFG^i$GYBW+)&B&2j znK5U;8*NARm^W9Llp)IUE}RwRpWS&kTuuA)v?wx7(u~#}m!@gMY&urMHKa)qLdo!n zA0OWY0bog8gux1iDNoImshJA1vrKKIYsF4vcWWz?z22G4mpKJempl0z)lWHPyy_anq}A}}d6fY8u4QBnXx(a98DeD^C5Bt&%&58m z9Iu`MC#D9l@LVC zoLichuh=J_OwN0#bd`OiD@8y*ZaB704bcylg#Aw>pqItKJN z@K&d(Unk9w{t!DS!V*G=bY4_Y5<>_VNzyRbyzncm)#xOqFbB^9Eb;3g4$+GUimg2^ z4AyV=*Khym4oak7BB#;2!_@L1mF=gpPZtNd{eEu$=T!EWRJj)&E-d#Jn8;w9$5I^Y zV}1{eV|0KX%KwKuAUr{;fWAl}1bE5!!{-IEgrlp3aT5AwNrgwIE1Sx9GGMC~*&>8_ zGrVOOlyg8j!h!ZJ?rHud)fFZ(27pFLvkBKUL~lv#jQPOX7I!_VHVq!T2mzg|Q{cU_ zCCd=Yb0C1hix+#b-;>v#s)Jj5{abs_lKan*6nm$a8zxoEY6qzceKgA0Z_c_pvR)YXq03LFyD^FOG5DF zSg3I#t_7v}dy9I+i~E;-<1T?$LbTpn4IIG}KJ^9!)b=~!AAeQUAUNcg^fG!g_`Q#q gC`XWj3p@cJN%{j_{uSXD>g!Vc(ffZRQ2xe$0nh22)&Kwi literal 0 HcmV?d00001 diff --git a/src/tools/__pycache__/gemini_client.cpython-312.pyc b/src/tools/__pycache__/gemini_client.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca3388161b6746540dbc6ef16b818f0068c8d87d GIT binary patch literal 1034 zcmZuv&ui3B5T2JG+il%8LS2x8Q0Sp#VHaAcEuw;GT}o{cM8ObvgyeP2noZW1th!4N z6&BQkc&vXyOZ_Xnda)E~dDxRDZ{i+=o}8C#bSVyG-pssj=FNPW&y$lAfMR^Q;ol_y z-=&j_HZ%s0(AWhJJQ6^QSPGH05~wZB(g-NPlxA8bPs~oHeN+#yrWA>)gQxYu(+`v^ zW)-@`Ec}qprb)z9eRX4PbB*4*zed+rA7f<9Y5Vjk>$({})5ByK%%Hyu3@qY-rFfc0 z`ovN_r4N?YRZU%#>a695{_VhLVdCa_vWhzV1}kZeWY{D?a}*_!DBC0-VjJk+`!4L0 zO;Z(mgN2SSj5-S$cM|_O+lV|C2)!i_QxRIsf-Qal3&k>dcJ^%Dy=Fg*8ISG6bv$O@ ziC%<3d#xV1c7 zkPlx%wFBSEGsor1VY#xmbW~n=qrICLU>yF>%ssr>g>7g`$T?|B{?5rimYVt6j36EW zFQRLzDG}l#&MDbq)Q!RfXBJw*UM8l(ONbJMHji5EWXKnvl(Ax-a&FE%JdU9F3aTAA zF{a=5-u8}-%Arx&yLx0S9vjPt#`2fqGB*%`%htmVCL3%NhDGP>8-bC=h)m14@iJCZ-!v z)CgE@*f;JAZck~{<8l;H>Lh*;*N~qrzriKpVmxy?oMjJQ#^Apu&WhKwo^NtVgIwu& k4%JVU5ONB0r!ap4#*Y5lcx8N;`U>;EHJz;d0#s>{KQG1UGXMYp literal 0 HcmV?d00001 diff --git a/src/tools/__pycache__/metadata_tool.cpython-312.pyc b/src/tools/__pycache__/metadata_tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..dc6db78acbb88df511ee97c49dc4f65f944aca09 GIT binary patch literal 1039 zcmYjQ&1(}u6o2!v+jLD!we<_XQbi<(R;+pv6{HanX?n0;Ldvr4Ov1`$H|}iEN(w#n zP(e^n_254sz4#}16g-Mh284oo@D|%DSUmV9n-w4I`H<$eEWTvr{zU-9Htb>Fa5m2Lo)R>?(E||0$RPa?^ zvkj0g&dJmhUg{YnJtiNgiRY(oYmj|1c{9tD@=U3b#H~{0_U!yzsS$)yX{&Gi9NEAZ z@G}kVtfE?nCM?q~sleh%fMu2vO!lSAHU|DO@6rbY3i4HByFA}P;8}?ykzHDW=iH}F z@aZDr;7Km1V1mD9FX=OfV}6+n>ovWL9_dGqplnz5km~rpR7$f8GZ!_TT_`g>Vru`_ zt#;a88W^4fp$n~8cwvw(m8rH>>ZKipxdAU_ns6*n+g`gZBT=?AS2AoQrtxIISs@M0 z4X?^vhLVxJJF{2n_moUjJ@q{y>y`LX6vm!Vb+3t_fj`xhtsn}fykIJkIEK1P{CbLm zQS}_yHQ{u7NwcIAJsF$(Z+g?18AdSWqFRBi@q=rH&HabAfeoKtyYY4W{9E#Id}gh( zS)6!2|8)LUmq9%q}0_#*L~RW!odnRR__y1`AN`DgE-PW z3{;vN#vi?b9n_EiR{VbkHsClB$@OFn4#gdgzRUgg1y4ktcmN%X$;K`Ri?@wBZPc|W1r#H PE9Bm94&m6>6(C>YEh3Luab$Ieu?ZTyy&z?}OVI7aNCJb}Wa30nN#g zwh8YOo(!H(M@9S50T)S_jowI}7WYgsM=+VknRHZa=oB}dCkZD_sGzHC7FnEoUk;DNN>WDD2;ky)DtWunJ)$L zGu4{(W!+|=o&NExTr+;r{nviq9O+M&4-|ZWt2<8@n~TSv?k{ieoZzeO!wYutda<|c zY%QBxm6E{NwaaD@q+!B>plk&}!f76B-VTCk9>yjjZYix;)6mwSM=o@s8gzT|hJra% zmetrc8Xb16vMHGQ37JWw|3Ald^O-5QBf3hir!DoQe`18tH|YGfG1~qG3hJDH0D+sG AcK`qY literal 0 HcmV?d00001 diff --git a/src/tools/__pycache__/vector_tool.cpython-312.pyc b/src/tools/__pycache__/vector_tool.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ecd6ab6a5aeba2bd8d237749b799aac6967c0a69 GIT binary patch literal 713 zcmY*XO=}ZD7@pakP10>cV=H3u;GPOgNq#`7UK$Y*a;f&PEDV#GNm$sAIx|tyQx5q7 z-s-_WApR8pK$w#tBHkjYm!5nln+P44=lz`L{b0Ve+aZE;^6Odl0weUp8QT+Vl+`gP zH%K6Xb5!62W8|TiNO*H3e2KsMbzb64NgVukMvN_aQ&l;yBQ<&(V-^@MY&we4&wiXqJeWkVi& zwxJC(6K!|?$G#wN6D{+RY66>fb)x9*)%j`qR!gN*!E{wuWsufQS46M@)w=;TIQ|qfqd3|_!e=R+5wp_gffEoJ!=;`9<)Cok21{|fuZixH-?h%_ g7N4slNP(Y;^h1E9k1_s Dict[str, Any]: + pass diff --git a/src/tools/dicom_download_tool.py b/src/tools/dicom_download_tool.py new file mode 100644 index 000000000..b011f6383 --- /dev/null +++ b/src/tools/dicom_download_tool.py @@ -0,0 +1,88 @@ +# src/tools/dicom_download_tool.py + +import os +from typing import List +from nbiatoolkit import NBIAClient + +DEFAULT_FILE_PATTERN = "%PatientID/%StudyInstanceUID/%SeriesInstanceUID/%InstanceNumber.dcm" + +def download_dicom_from_tcia( + collection: str, + output_dir: str = "src/data", + n_parallel: int = 5 +) -> List[str]: + """ + Downloads DICOM series from TCIA into src/data directory. + Returns list of downloaded DICOM file paths. + """ + + os.makedirs(output_dir, exist_ok=True) + + downloaded_files = [] + + with NBIAClient(return_type="dataframe") as client: + patients = client.getPatients(Collection=collection) + if patients.empty: + raise RuntimeError(f"No patients found for collection: {collection}") + + # Pick first patient for demo (agent can choose later) + patient = patients.iloc[0] + + series_df = client.getSeries(PatientID=patient.PatientId) + if series_df.empty: + raise RuntimeError("No series found for patient") + + series_uid = series_df.SeriesInstanceUID.iloc[0] + + client.downloadSeries( + series_uid, + output_dir, + DEFAULT_FILE_PATTERN, + n_parallel + ) + + + +def file_generator(DOWNLOAD_PATH, TARGET_COLLECTION): + """ + A generator that yields the full path of every file in a directory + and its subdirectories. + """ + + download_dir = os.path.join(DOWNLOAD_PATH) + print(f"Download Dir: {download_dir}") + + dicom_files = get_all_files_os_walk(download_dir) + print(f"Successfully Found files: {len(dicom_files)}") + + for f in dicom_files: + yield f + + +def get_all_files_os_walk(directory_path): + file_list = [] + for root, directories, files in os.walk(directory_path): + for filename in files: + # Construct the full file path + full_path = os.path.join(root, filename) + if full_path.endswith('.dcm'): + file_list.append(full_path) + return file_list + +def get_all_files_map_os_walk(directory_path): + file_list = {} + for root, directories, files in os.walk(directory_path): + for filename in files: + # Construct the full file path + full_path = os.path.join(root, filename) + if full_path.endswith('.dcm'): + file_list.update({filename: full_path}) + return file_list + +def get_all_files_os_walk_generator(directory_path): + for root, directories, files in os.walk(directory_path): + for filename in files: + # Construct the full file path + full_path = os.path.join(root, filename) + if full_path.endswith('.dcm'): + yield full_path diff --git a/src/tools/dicom_ontology_llm_tool.py b/src/tools/dicom_ontology_llm_tool.py new file mode 100644 index 000000000..310ea73f6 --- /dev/null +++ b/src/tools/dicom_ontology_llm_tool.py @@ -0,0 +1,19 @@ +# src/dicom_agent/tools/dicom_ontology_llm_tool.py +from llm.gemini_client import GeminiClient + + +PROMPT = """ +You are a medical ontology assistant. +Translate the following DICOM metadata into plain English +using anatomy and imaging terminology. + + +Metadata: +{metadata} +""" + + +async def explain_metadata(metadata: dict) -> str: + gemini = GeminiClient() + prompt = PROMPT.format(metadata=metadata) + return await gemini.generate(prompt) \ No newline at end of file diff --git a/src/tools/dicom_ontology_presenter.py b/src/tools/dicom_ontology_presenter.py new file mode 100644 index 000000000..412299e02 --- /dev/null +++ b/src/tools/dicom_ontology_presenter.py @@ -0,0 +1,58 @@ +# src/tools/dicom_ontology_presenter.py + +import pydicom +from typing import Dict, Any + +ONTOLOGY_LOOKUP = { + "CT": "a standard X-ray-based scan (CT scan).", + "MR": "a scan using magnetic fields (MRI).", + "DX": "a digital X-ray image.", + "CHEST": "the chest and lungs.", + "ABDOMEN": "the abdominal cavity.", + "HEAD": "the head and brain.", + "T1": "an MRI sequence highlighting fatty tissue.", +} + +def translate(term: str) -> str: + if term in ONTOLOGY_LOOKUP: + return ONTOLOGY_LOOKUP[term] + for k, v in ONTOLOGY_LOOKUP.items(): + if k in term: + return v + return f"{term} (a specialized medical term)" + +def extract_metadata(ds: pydicom.Dataset) -> Dict[str, Any]: + return { + "PatientID": getattr(ds, "PatientID", "N/A"), + "StudyDate": getattr(ds, "StudyDate", "N/A"), + "Modality": getattr(ds, "Modality", "N/A"), + "BodyPart": getattr(ds, "BodyPartExamined", "N/A"), + "Protocol": getattr(ds, "ProtocolName", + getattr(ds, "SeriesDescription", "Generic Scan")) + } + +def present_dicom_using_ontology(dicom_file: str) -> str: + ds = pydicom.dcmread(dicom_file, stop_before_pixels=True) + meta = extract_metadata(ds) + + return f""" +--- DICOM Image Explanation --- + +Image Type: +• {meta['Modality']} — {translate(meta['Modality'])} + +Anatomy: +• {meta['BodyPart']} — {translate(meta['BodyPart'])} + +Scan Technique: +• {meta['Protocol']} — {translate(meta['Protocol'])} + +Study Date: +• {meta['StudyDate']} + +Patient Context: +• Internal ID: {meta['PatientID']} + +This description translates technical imaging metadata into +human-readable clinical context. +""".strip() diff --git a/src/tools/dicom_vector_index_tool.py b/src/tools/dicom_vector_index_tool.py new file mode 100644 index 000000000..551bcf00e --- /dev/null +++ b/src/tools/dicom_vector_index_tool.py @@ -0,0 +1,20 @@ +# src/dicom_agent/tools/dicom_vector_index_tool.py +import pydicom +from memory.vector_store import VectorStore +from utils.metadata import extract_metadata, metadata_to_text +from tools.dicom_ontology_presenter import present_dicom_using_ontology + + +async def index_dicom_files_to_vector_db(dicom_files: list[str], store: VectorStore): + texts, metadatas = [], [] + for f in dicom_files: + ds = pydicom.dcmread(f, stop_before_pixels=True) + meta = extract_metadata(ds) + ont_meta = present_dicom_using_ontology(f) + texts.append(metadata_to_text(ont_meta)) + metadatas.append({"path": f, **meta}) + + + await store.add_texts(texts, metadatas) + + diff --git a/src/tools/download_tool.py b/src/tools/download_tool.py new file mode 100644 index 000000000..24f050be1 --- /dev/null +++ b/src/tools/download_tool.py @@ -0,0 +1,33 @@ +# tools/download_tool.py +from tools.base import Tool +from tools.dicom_download_tool import download_dicom_from_tcia, get_all_files_os_walk_generator + +class DownloadTool(Tool): + name = "download_files" + description = "Download files from a remote endpoint" + + def __init__(self): + self.TARGET_COLLECTION = 'LGG-18' # A small, publicly accessible collection + self.TARGET_COLLECTION = 'COVID-19-AR' + self.TARGET_COLLECTION = 'TCGA-KIRC' + self.DOWNLOAD_PATH = 'data/tcia_presentation_dat' + + def run(self, url: str, output_dir: str): + # placeholder logic + + download_dicom_from_tcia(collection=self.TARGET_COLLECTION, output_dir=self.DOWNLOAD_PATH) + file_map = self.get_all_files_map_os_walk(directory_path=self.DOWNLOAD_PATH) + return file_map + + + def get_all_files_map_os_walk(self, directory_path): + file_list = [] + + for f in get_all_files_os_walk_generator(directory_path=self.DOWNLOAD_PATH): + file_list.append(f) + + + return { + "status": "success", + "files": file_list + } diff --git a/src/tools/gemini_client.py b/src/tools/gemini_client.py new file mode 100644 index 000000000..6142d2aaf --- /dev/null +++ b/src/tools/gemini_client.py @@ -0,0 +1,10 @@ +import google.generativeai as genai +from utils.config import GEMINI_API_KEY +genai.configure(api_key=GEMINI_API_KEY) + +class GeminiClient: + def __init__(self, model="gemini-1.5-flash"): + self.model = genai.GenerativeModel(model) + + def generate(self, prompt): + return self.model.generate_content(prompt).text diff --git a/src/tools/image_to_dicom.py b/src/tools/image_to_dicom.py new file mode 100644 index 000000000..ea3e83dc4 --- /dev/null +++ b/src/tools/image_to_dicom.py @@ -0,0 +1,133 @@ +""" +Agent tool: Convert an image file into a DICOM (Secondary Capture). + +Use cases: +- AI-generated images → PACS-compatible DICOM +- Annotation overlays → DICOM +- Synthetic data generation +- Healthcare RAG / imaging pipelines +""" + +from typing import Dict, Any +from tools.base import Tool + +import datetime +import numpy as np +from PIL import Image +import pydicom +from pydicom.dataset import Dataset, FileDataset +from pydicom.uid import ( + ExplicitVRLittleEndian, + SecondaryCaptureImageStorage, + generate_uid +) + + +class ImageToDICOMTool(Tool): + """ + Agent-compatible image → DICOM conversion tool. + """ + + name = "image_to_dicom" + description = ( + "Convert a standard image (JPEG/PNG) into a DICOM Secondary Capture file. " + "Useful for AI annotations, synthetic images, and imaging pipelines." + ) + + # ========================================================== + # Tool Entry Point + # ========================================================== + + def run( + self, + image_path: str, + output_dcm: str, + patient_name: str = "DOE^JOHN", + patient_id: str = "123456", + study_description: str = "Image converted to DICOM", + modality: str = "OT" + ) -> Dict[str, Any]: + """ + Convert image to DICOM and save to disk. + """ + + self._image_to_dicom( + image_path=image_path, + output_dcm=output_dcm, + patient_name=patient_name, + patient_id=patient_id, + study_description=study_description, + modality=modality + ) + + return { + "status": "success", + "output_dcm": output_dcm, + "patient_id": patient_id, + "modality": modality + } + + # ========================================================== + # Core Logic + # ========================================================== + + def _image_to_dicom( + self, + image_path: str, + output_dcm: str, + patient_name: str, + patient_id: str, + study_description: str, + modality: str + ): + # Load image + img = Image.open(image_path).convert("RGB") + pixel_array = np.array(img) + + # ---- File Meta ---- + file_meta = Dataset() + file_meta.MediaStorageSOPClassUID = SecondaryCaptureImageStorage + file_meta.MediaStorageSOPInstanceUID = generate_uid() + file_meta.TransferSyntaxUID = ExplicitVRLittleEndian + file_meta.ImplementationClassUID = generate_uid() + + # ---- DICOM Dataset ---- + ds = FileDataset( + output_dcm, + {}, + file_meta=file_meta, + preamble=b"\0" * 128 + ) + + # ---- Patient / Study ---- + ds.PatientName = patient_name + ds.PatientID = patient_id + ds.StudyInstanceUID = generate_uid() + ds.SeriesInstanceUID = generate_uid() + ds.SOPInstanceUID = file_meta.MediaStorageSOPInstanceUID + ds.SOPClassUID = file_meta.MediaStorageSOPClassUID + + ds.Modality = modality + ds.StudyDescription = study_description + ds.SeriesDescription = "Image-to-DICOM Conversion" + + now = datetime.datetime.now() + ds.StudyDate = now.strftime("%Y%m%d") + ds.StudyTime = now.strftime("%H%M%S") + + # ---- Image Tags ---- + ds.Rows, ds.Columns = pixel_array.shape[:2] + ds.SamplesPerPixel = 3 + ds.PhotometricInterpretation = "RGB" + ds.PlanarConfiguration = 0 + ds.BitsAllocated = 8 + ds.BitsStored = 8 + ds.HighBit = 7 + ds.PixelRepresentation = 0 + + ds.PixelData = pixel_array.tobytes() + + # ---- Save ---- + ds.is_little_endian = True + ds.is_implicit_VR = False + ds.save_as(output_dcm) diff --git a/src/tools/metadata_tool.py b/src/tools/metadata_tool.py new file mode 100644 index 000000000..15c593e80 --- /dev/null +++ b/src/tools/metadata_tool.py @@ -0,0 +1,17 @@ +# tools/metadata_tool.py +from tools.base import Tool +from tools.dicom_ontology_presenter import present_dicom_using_ontolog + +class MetadataTool(Tool): + name = "extract_metadata" + description = "Extract metadata from DICOM files" + + def run(self, files: list): + + return { + "status": "success", + "metadata": [ + {"file": f, "patient_id": "123", "modality": "CT"} + for f in files + ] + } diff --git a/src/tools/search_tool.py b/src/tools/search_tool.py new file mode 100644 index 000000000..267993c5d --- /dev/null +++ b/src/tools/search_tool.py @@ -0,0 +1,6 @@ +import requests + +class SearchTool: + def search(self, query): + url = f"https://duckduckgo.com/?q={query}&format=json" + return f"Search results for: {query} (mocked)" diff --git a/src/tools/test.py b/src/tools/test.py new file mode 100644 index 000000000..e96ee0057 --- /dev/null +++ b/src/tools/test.py @@ -0,0 +1 @@ +def download_dicom_from_tcia(*args, **kwargs): return [] \ No newline at end of file diff --git a/src/tools/vector_tool.py b/src/tools/vector_tool.py new file mode 100644 index 000000000..a083b9de6 --- /dev/null +++ b/src/tools/vector_tool.py @@ -0,0 +1,15 @@ +# tools/vector_tool.py +from tools.base import Tool +from tools.dicom_vector_index_tool import VectorStore + +class VectorStoreTool(Tool): + name = "create_vector_store" + description = "Create vector embeddings from metadata" + + def run(self, metadata: list): + + + return { + "status": "success", + "vector_db": "vector_db_id_abc" + } diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/utils/__pycache__/__init__.cpython-312.pyc b/src/utils/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d99249b3eef6be9de0ce49a8c7ebc8238ba4a5c9 GIT binary patch literal 167 zcmX@j%ge<81O-oRGePuY5P=Rpvj9b=GgLBYGWxA#C}INgK7-W!a&xwd2`x@7Dvl{h zPE1LSamg>w%gIknDUL}@Pt7aIOx8_L&CSfq)J@FPElN#HE{Q2FN{%Tl$;>H^iH`?L nl*GsD6;%G>u*uC&Da}c>D`Ev2%Lv59AjU^#Mn=XWW*`dy^TjJJ literal 0 HcmV?d00001 diff --git a/src/utils/__pycache__/config.cpython-312.pyc b/src/utils/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..5cf657c6f0a48013da8a23c93eb7827590f1b5e7 GIT binary patch literal 404 zcmX@j%ge<81pJ3=GuHs=#~=<2Fhd!ioBqye%#<=8{=YcFQj!8^U1u9S0 zO;63u%*)hG%+xJPO-wF{DK1KmDJ{v&DUM0b&r8cp*DI*}#bJ}1pHiBWYFESqG!*2| zVpAaTftit!@iqhJT?Vn--2{Pgt9 zy!2b#Nr}ao$8ZJyd6~M2nGl;|ii?tCN=q_xien%q=@nG|;;_lh zPbtkwwJYKU8Vd4iu_2K7z|6?Vc$@Ra_H?kFR19braXW?(6 literal 0 HcmV?d00001 diff --git a/src/utils/config.py b/src/utils/config.py new file mode 100644 index 000000000..2d1151fbb --- /dev/null +++ b/src/utils/config.py @@ -0,0 +1,6 @@ +import os +from dotenv import load_dotenv +load_dotenv() +GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") +if not GEMINI_API_KEY: + raise RuntimeError("Missing GEMINI_API_KEY") diff --git a/src/utils/logger.py b/src/utils/logger.py new file mode 100644 index 000000000..3a039e852 --- /dev/null +++ b/src/utils/logger.py @@ -0,0 +1,3 @@ +import logging +logging.basicConfig(level=logging.INFO, format="%(asctime)s | %(levelname)s | %(message)s") +logger = logging.getLogger("agentic-react") diff --git a/src/utils/metadata.py b/src/utils/metadata.py new file mode 100644 index 000000000..008ad291a --- /dev/null +++ b/src/utils/metadata.py @@ -0,0 +1,149 @@ +""" +Metadata extraction and normalization tool for agentic AI pipelines. + +Features: +- DICOM metadata extraction (via pydicom) +- Generic file metadata fallback +- Text normalization for LLMs / embeddings / RAG +""" + +from typing import List, Dict, Any +from pathlib import Path +from datetime import datetime + +try: + import pydicom +except ImportError: + pydicom = None + +from tools.base import Tool + + +class MetadataTool(Tool): + """ + Agent-compatible metadata extraction tool. + """ + + name = "extract_metadata" + description = ( + "Extract structured metadata from DICOM or generic files and " + "optionally convert it into LLM-friendly text." + ) + + # ============================================================== + # Public Agent Methods + # ============================================================== + + def run(self, files: List[str]) -> Dict[str, Any]: + """ + Required Tool interface. + """ + metadata = self.extract_metadata(files) + + return { + "status": "success", + "metadata": metadata, + "count": len(metadata), + "extracted_at": datetime.utcnow().isoformat() + } + + def extract_metadata(self, files: List[str]) -> List[Dict[str, Any]]: + """ + Extract structured metadata from a list of files. + """ + results = [] + + for file_path in files: + path = Path(file_path) + + if not path.exists(): + results.append({ + "file": file_path, + "error": "File not found" + }) + continue + + if path.suffix.lower() == ".dcm": + results.append(self._extract_dicom_metadata(path)) + else: + results.append(self._extract_generic_metadata(path)) + + return results + + def metadata_to_text(self, metadata: List[Dict[str, Any]]) -> List[str]: + """ + Convert structured metadata into natural language text suitable + for embeddings, RAG, or LLM reasoning. + """ + texts = [] + + for item in metadata: + if "error" in item: + texts.append( + f"File {item.get('file')} encountered an error: {item.get('error')}" + ) + continue + + lines = [] + + for key, value in item.items(): + if value is None or key in {"file", "type"}: + continue + lines.append(f"{key.replace('_', ' ')}: {value}") + + summary = f"Metadata for {item.get('file')}:\n" + "; ".join(lines) + texts.append(summary) + + return texts + + # ============================================================== + # Internal Helpers + # ============================================================== + + def _extract_dicom_metadata(self, path: Path) -> Dict[str, Any]: + if pydicom is None: + return { + "file": str(path), + "type": "dicom", + "error": "pydicom not installed" + } + + try: + ds = pydicom.dcmread(str(path), stop_before_pixels=True) + + return { + "file": str(path), + "type": "dicom", + "patient_id": getattr(ds, "PatientID", None), + "patient_sex": getattr(ds, "PatientSex", None), + "patient_birth_date": getattr(ds, "PatientBirthDate", None), + "study_instance_uid": getattr(ds, "StudyInstanceUID", None), + "series_instance_uid": getattr(ds, "SeriesInstanceUID", None), + "study_date": getattr(ds, "StudyDate", None), + "modality": getattr(ds, "Modality", None), + "body_part_examined": getattr(ds, "BodyPartExamined", None), + "manufacturer": getattr(ds, "Manufacturer", None), + "institution_name": getattr(ds, "InstitutionName", None), + "rows": getattr(ds, "Rows", None), + "columns": getattr(ds, "Columns", None) + } + + except Exception as e: + return { + "file": str(path), + "type": "dicom", + "error": str(e) + } + + def _extract_generic_metadata(self, path: Path) -> Dict[str, Any]: + stat = path.stat() + + return { + "file": str(path), + "type": "generic", + "filename": path.name, + "extension": path.suffix, + "size_bytes": stat.st_size, + "created_at": datetime.fromtimestamp(stat.st_ctime).isoformat(), + "modified_at": datetime.fromtimestamp(stat.st_mtime).isoformat() + } diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/test_basic.py b/tests/test_basic.py new file mode 100644 index 000000000..d1987657c --- /dev/null +++ b/tests/test_basic.py @@ -0,0 +1,4 @@ + +def test_import(): + import dicom_agent + assert True diff --git a/tests/test_ontology_explanation.py b/tests/test_ontology_explanation.py new file mode 100644 index 000000000..606c1f893 --- /dev/null +++ b/tests/test_ontology_explanation.py @@ -0,0 +1,15 @@ +# tests/test_ontology_explanation.py + +import asyncio +from tools.dicom_ontology_llm_tool import explain_metadata + +async def test_explanation_guardrails(): + metadata = { + "Modality": "CT", + "BodyPart": "CHEST" + } + + explanation = await explain_metadata(metadata) + + assert "CT" in explanation + assert "diagnostic" not in explanation.lower() # guardrail diff --git a/tests/test_vector_index.py b/tests/test_vector_index.py new file mode 100644 index 000000000..a0954e6e3 --- /dev/null +++ b/tests/test_vector_index.py @@ -0,0 +1,19 @@ +# tests/test_vector_index.py + +import asyncio +from memory.vector_store import VectorStore +from tools.dicom_vector_index_tool import index_dicom_to_vector_db + +async def test_indexing_pipeline(tmp_path): + store = VectorStore(dim=384) + + # fake DICOM metadata text + texts = ["CT Chest Lung Nodule"] + metas = [{"Modality": "CT", "BodyPart": "CHEST"}] + + await store.add_texts(texts, metas) + + results = await store.similarity_search("lung CT") + + assert len(results) > 0 + assert "CT" in results[0].metadata["Modality"]