amd-gaia 0.14.2__py3-none-any.whl → 0.14.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (788) hide show
  1. {amd_gaia-0.14.2.dist-info → amd_gaia-0.14.3.dist-info}/METADATA +5 -2
  2. amd_gaia-0.14.3.dist-info/RECORD +168 -0
  3. {amd_gaia-0.14.2.dist-info → amd_gaia-0.14.3.dist-info}/entry_points.txt +1 -0
  4. gaia/__init__.py +28 -1
  5. gaia/agents/__init__.py +1 -1
  6. gaia/agents/base/__init__.py +1 -1
  7. gaia/agents/base/agent.py +110 -33
  8. gaia/agents/base/api_agent.py +1 -1
  9. gaia/agents/base/console.py +399 -15
  10. gaia/agents/base/errors.py +237 -0
  11. gaia/agents/base/mcp_agent.py +1 -1
  12. gaia/agents/base/tools.py +1 -1
  13. gaia/agents/blender/agent.py +1 -1
  14. gaia/agents/blender/agent_simple.py +1 -1
  15. gaia/agents/blender/app.py +1 -1
  16. gaia/agents/blender/app_simple.py +1 -1
  17. gaia/agents/blender/core/__init__.py +1 -1
  18. gaia/agents/blender/core/materials.py +1 -1
  19. gaia/agents/blender/core/objects.py +1 -1
  20. gaia/agents/blender/core/rendering.py +1 -1
  21. gaia/agents/blender/core/scene.py +1 -1
  22. gaia/agents/blender/core/view.py +1 -1
  23. gaia/agents/chat/__init__.py +1 -1
  24. gaia/agents/chat/agent.py +36 -153
  25. gaia/agents/chat/app.py +1 -1
  26. gaia/agents/chat/session.py +1 -1
  27. gaia/agents/chat/tools/__init__.py +1 -1
  28. gaia/agents/chat/tools/file_tools.py +1 -1
  29. gaia/agents/chat/tools/rag_tools.py +1 -1
  30. gaia/agents/chat/tools/shell_tools.py +1 -1
  31. gaia/agents/code/__init__.py +1 -1
  32. gaia/agents/code/agent.py +3 -1
  33. gaia/agents/code/orchestration/__init__.py +1 -1
  34. gaia/agents/code/orchestration/checklist_executor.py +1 -1
  35. gaia/agents/code/orchestration/checklist_generator.py +1 -1
  36. gaia/agents/code/orchestration/factories/__init__.py +1 -1
  37. gaia/agents/code/orchestration/factories/base.py +1 -1
  38. gaia/agents/code/orchestration/factories/nextjs_factory.py +1 -1
  39. gaia/agents/code/orchestration/factories/python_factory.py +1 -1
  40. gaia/agents/code/orchestration/orchestrator.py +212 -1
  41. gaia/agents/code/orchestration/project_analyzer.py +1 -1
  42. gaia/agents/code/orchestration/steps/__init__.py +1 -1
  43. gaia/agents/code/orchestration/steps/base.py +1 -1
  44. gaia/agents/code/orchestration/steps/error_handler.py +1 -1
  45. gaia/agents/code/orchestration/steps/nextjs.py +1 -1
  46. gaia/agents/code/orchestration/steps/python.py +1 -1
  47. gaia/agents/code/orchestration/template_catalog.py +1 -1
  48. gaia/agents/code/orchestration/workflows/__init__.py +1 -1
  49. gaia/agents/code/orchestration/workflows/base.py +1 -1
  50. gaia/agents/code/orchestration/workflows/nextjs.py +1 -1
  51. gaia/agents/code/orchestration/workflows/python.py +1 -1
  52. gaia/agents/code/prompts/__init__.py +1 -1
  53. gaia/agents/code/prompts/base_prompt.py +1 -1
  54. gaia/agents/code/prompts/code_patterns.py +1 -1
  55. gaia/agents/code/prompts/nextjs_prompt.py +1 -1
  56. gaia/agents/code/prompts/python_prompt.py +1 -1
  57. gaia/agents/code/schema_inference.py +1 -1
  58. gaia/agents/code/system_prompt.py +1 -1
  59. gaia/agents/code/tools/__init__.py +1 -1
  60. gaia/agents/code/tools/cli_tools.py +1 -1
  61. gaia/agents/code/tools/code_formatting.py +1 -1
  62. gaia/agents/code/tools/code_tools.py +1 -1
  63. gaia/agents/code/tools/error_fixing.py +1 -1
  64. gaia/agents/code/tools/external_tools.py +1 -1
  65. gaia/agents/code/tools/prisma_tools.py +1 -1
  66. gaia/agents/code/tools/project_management.py +1 -1
  67. gaia/agents/code/tools/testing.py +1 -1
  68. gaia/agents/code/tools/typescript_tools.py +1 -1
  69. gaia/agents/code/tools/validation_parsing.py +1 -1
  70. gaia/agents/code/tools/validation_tools.py +5 -2
  71. gaia/agents/code/tools/web_dev_tools.py +1 -2
  72. gaia/agents/docker/__init__.py +1 -1
  73. gaia/agents/emr/__init__.py +8 -0
  74. gaia/agents/emr/agent.py +1506 -0
  75. gaia/agents/emr/cli.py +1322 -0
  76. gaia/agents/emr/constants.py +475 -0
  77. gaia/agents/emr/dashboard/__init__.py +4 -0
  78. gaia/agents/emr/dashboard/server.py +1974 -0
  79. gaia/agents/routing/__init__.py +1 -1
  80. gaia/agents/routing/agent.py +65 -7
  81. gaia/agents/routing/system_prompt.py +1 -1
  82. gaia/api/__init__.py +1 -1
  83. gaia/api/agent_registry.py +1 -1
  84. gaia/api/app.py +1 -1
  85. gaia/api/openai_server.py +1 -1
  86. gaia/api/schemas.py +1 -1
  87. gaia/api/sse_handler.py +5 -2
  88. gaia/apps/__init__.py +1 -1
  89. gaia/apps/llm/__init__.py +1 -1
  90. gaia/audio/__init__.py +1 -1
  91. gaia/audio/audio_client.py +1 -1
  92. gaia/audio/audio_recorder.py +1 -1
  93. gaia/audio/kokoro_tts.py +1 -1
  94. gaia/audio/whisper_asr.py +1 -1
  95. gaia/chat/__init__.py +1 -1
  96. gaia/chat/prompts.py +1 -1
  97. gaia/chat/sdk.py +25 -0
  98. gaia/cli.py +2 -2
  99. gaia/database/__init__.py +10 -0
  100. gaia/database/agent.py +176 -0
  101. gaia/database/mixin.py +290 -0
  102. gaia/database/testing.py +64 -0
  103. gaia/eval/batch_experiment.py +1 -1
  104. gaia/eval/claude.py +1 -1
  105. gaia/eval/config.py +1 -1
  106. gaia/eval/email_generator.py +1 -1
  107. gaia/eval/eval.py +1 -1
  108. gaia/eval/groundtruth.py +1 -1
  109. gaia/eval/transcript_generator.py +1 -1
  110. gaia/eval/webapp/public/app.js +1 -1
  111. gaia/eval/webapp/server.js +1 -1
  112. gaia/eval/webapp/test-setup.js +1 -1
  113. gaia/llm/__init__.py +1 -1
  114. gaia/llm/lemonade_client.py +149 -11
  115. gaia/llm/lemonade_manager.py +36 -11
  116. gaia/llm/llm_client.py +1 -1
  117. gaia/llm/vlm_client.py +93 -18
  118. gaia/logger.py +1 -1
  119. gaia/mcp/agent_mcp_server.py +1 -1
  120. gaia/mcp/blender_mcp_client.py +1 -1
  121. gaia/mcp/blender_mcp_server.py +1 -1
  122. gaia/mcp/context7_cache.py +1 -1
  123. gaia/mcp/servers/__init__.py +1 -1
  124. gaia/mcp/servers/docker_mcp.py +1 -1
  125. gaia/security.py +1 -1
  126. gaia/testing/__init__.py +87 -0
  127. gaia/testing/assertions.py +330 -0
  128. gaia/testing/fixtures.py +333 -0
  129. gaia/testing/mocks.py +493 -0
  130. gaia/util.py +1 -1
  131. gaia/utils/__init__.py +33 -0
  132. gaia/utils/file_watcher.py +675 -0
  133. gaia/utils/parsing.py +223 -0
  134. gaia/version.py +2 -2
  135. amd_gaia-0.14.2.dist-info/RECORD +0 -800
  136. gaia/eval/webapp/node_modules/.bin/mime +0 -16
  137. gaia/eval/webapp/node_modules/.bin/mime.cmd +0 -17
  138. gaia/eval/webapp/node_modules/.bin/mime.ps1 +0 -28
  139. gaia/eval/webapp/node_modules/.package-lock.json +0 -865
  140. gaia/eval/webapp/node_modules/accepts/HISTORY.md +0 -243
  141. gaia/eval/webapp/node_modules/accepts/LICENSE +0 -23
  142. gaia/eval/webapp/node_modules/accepts/README.md +0 -140
  143. gaia/eval/webapp/node_modules/accepts/index.js +0 -238
  144. gaia/eval/webapp/node_modules/accepts/package.json +0 -47
  145. gaia/eval/webapp/node_modules/array-flatten/LICENSE +0 -21
  146. gaia/eval/webapp/node_modules/array-flatten/README.md +0 -43
  147. gaia/eval/webapp/node_modules/array-flatten/array-flatten.js +0 -64
  148. gaia/eval/webapp/node_modules/array-flatten/package.json +0 -39
  149. gaia/eval/webapp/node_modules/body-parser/HISTORY.md +0 -672
  150. gaia/eval/webapp/node_modules/body-parser/LICENSE +0 -23
  151. gaia/eval/webapp/node_modules/body-parser/README.md +0 -476
  152. gaia/eval/webapp/node_modules/body-parser/SECURITY.md +0 -25
  153. gaia/eval/webapp/node_modules/body-parser/index.js +0 -156
  154. gaia/eval/webapp/node_modules/body-parser/lib/read.js +0 -205
  155. gaia/eval/webapp/node_modules/body-parser/lib/types/json.js +0 -247
  156. gaia/eval/webapp/node_modules/body-parser/lib/types/raw.js +0 -101
  157. gaia/eval/webapp/node_modules/body-parser/lib/types/text.js +0 -121
  158. gaia/eval/webapp/node_modules/body-parser/lib/types/urlencoded.js +0 -307
  159. gaia/eval/webapp/node_modules/body-parser/package.json +0 -56
  160. gaia/eval/webapp/node_modules/bytes/History.md +0 -97
  161. gaia/eval/webapp/node_modules/bytes/LICENSE +0 -23
  162. gaia/eval/webapp/node_modules/bytes/Readme.md +0 -152
  163. gaia/eval/webapp/node_modules/bytes/index.js +0 -170
  164. gaia/eval/webapp/node_modules/bytes/package.json +0 -42
  165. gaia/eval/webapp/node_modules/call-bind-apply-helpers/.eslintrc +0 -17
  166. gaia/eval/webapp/node_modules/call-bind-apply-helpers/.github/FUNDING.yml +0 -12
  167. gaia/eval/webapp/node_modules/call-bind-apply-helpers/.nycrc +0 -9
  168. gaia/eval/webapp/node_modules/call-bind-apply-helpers/CHANGELOG.md +0 -30
  169. gaia/eval/webapp/node_modules/call-bind-apply-helpers/LICENSE +0 -21
  170. gaia/eval/webapp/node_modules/call-bind-apply-helpers/README.md +0 -62
  171. gaia/eval/webapp/node_modules/call-bind-apply-helpers/actualApply.d.ts +0 -1
  172. gaia/eval/webapp/node_modules/call-bind-apply-helpers/actualApply.js +0 -10
  173. gaia/eval/webapp/node_modules/call-bind-apply-helpers/applyBind.d.ts +0 -19
  174. gaia/eval/webapp/node_modules/call-bind-apply-helpers/applyBind.js +0 -10
  175. gaia/eval/webapp/node_modules/call-bind-apply-helpers/functionApply.d.ts +0 -1
  176. gaia/eval/webapp/node_modules/call-bind-apply-helpers/functionApply.js +0 -4
  177. gaia/eval/webapp/node_modules/call-bind-apply-helpers/functionCall.d.ts +0 -1
  178. gaia/eval/webapp/node_modules/call-bind-apply-helpers/functionCall.js +0 -4
  179. gaia/eval/webapp/node_modules/call-bind-apply-helpers/index.d.ts +0 -64
  180. gaia/eval/webapp/node_modules/call-bind-apply-helpers/index.js +0 -15
  181. gaia/eval/webapp/node_modules/call-bind-apply-helpers/package.json +0 -85
  182. gaia/eval/webapp/node_modules/call-bind-apply-helpers/reflectApply.d.ts +0 -3
  183. gaia/eval/webapp/node_modules/call-bind-apply-helpers/reflectApply.js +0 -4
  184. gaia/eval/webapp/node_modules/call-bind-apply-helpers/test/index.js +0 -63
  185. gaia/eval/webapp/node_modules/call-bind-apply-helpers/tsconfig.json +0 -9
  186. gaia/eval/webapp/node_modules/call-bound/.eslintrc +0 -13
  187. gaia/eval/webapp/node_modules/call-bound/.github/FUNDING.yml +0 -12
  188. gaia/eval/webapp/node_modules/call-bound/.nycrc +0 -9
  189. gaia/eval/webapp/node_modules/call-bound/CHANGELOG.md +0 -42
  190. gaia/eval/webapp/node_modules/call-bound/LICENSE +0 -21
  191. gaia/eval/webapp/node_modules/call-bound/README.md +0 -53
  192. gaia/eval/webapp/node_modules/call-bound/index.d.ts +0 -94
  193. gaia/eval/webapp/node_modules/call-bound/index.js +0 -19
  194. gaia/eval/webapp/node_modules/call-bound/package.json +0 -99
  195. gaia/eval/webapp/node_modules/call-bound/test/index.js +0 -61
  196. gaia/eval/webapp/node_modules/call-bound/tsconfig.json +0 -10
  197. gaia/eval/webapp/node_modules/content-disposition/HISTORY.md +0 -60
  198. gaia/eval/webapp/node_modules/content-disposition/LICENSE +0 -22
  199. gaia/eval/webapp/node_modules/content-disposition/README.md +0 -142
  200. gaia/eval/webapp/node_modules/content-disposition/index.js +0 -458
  201. gaia/eval/webapp/node_modules/content-disposition/package.json +0 -44
  202. gaia/eval/webapp/node_modules/content-type/HISTORY.md +0 -29
  203. gaia/eval/webapp/node_modules/content-type/LICENSE +0 -22
  204. gaia/eval/webapp/node_modules/content-type/README.md +0 -94
  205. gaia/eval/webapp/node_modules/content-type/index.js +0 -225
  206. gaia/eval/webapp/node_modules/content-type/package.json +0 -42
  207. gaia/eval/webapp/node_modules/cookie/LICENSE +0 -24
  208. gaia/eval/webapp/node_modules/cookie/README.md +0 -317
  209. gaia/eval/webapp/node_modules/cookie/SECURITY.md +0 -25
  210. gaia/eval/webapp/node_modules/cookie/index.js +0 -334
  211. gaia/eval/webapp/node_modules/cookie/package.json +0 -44
  212. gaia/eval/webapp/node_modules/cookie-signature/.npmignore +0 -4
  213. gaia/eval/webapp/node_modules/cookie-signature/History.md +0 -38
  214. gaia/eval/webapp/node_modules/cookie-signature/Readme.md +0 -42
  215. gaia/eval/webapp/node_modules/cookie-signature/index.js +0 -51
  216. gaia/eval/webapp/node_modules/cookie-signature/package.json +0 -18
  217. gaia/eval/webapp/node_modules/debug/.coveralls.yml +0 -1
  218. gaia/eval/webapp/node_modules/debug/.eslintrc +0 -11
  219. gaia/eval/webapp/node_modules/debug/.npmignore +0 -9
  220. gaia/eval/webapp/node_modules/debug/.travis.yml +0 -14
  221. gaia/eval/webapp/node_modules/debug/CHANGELOG.md +0 -362
  222. gaia/eval/webapp/node_modules/debug/LICENSE +0 -19
  223. gaia/eval/webapp/node_modules/debug/Makefile +0 -50
  224. gaia/eval/webapp/node_modules/debug/README.md +0 -312
  225. gaia/eval/webapp/node_modules/debug/component.json +0 -19
  226. gaia/eval/webapp/node_modules/debug/karma.conf.js +0 -70
  227. gaia/eval/webapp/node_modules/debug/node.js +0 -1
  228. gaia/eval/webapp/node_modules/debug/package.json +0 -49
  229. gaia/eval/webapp/node_modules/debug/src/browser.js +0 -185
  230. gaia/eval/webapp/node_modules/debug/src/debug.js +0 -202
  231. gaia/eval/webapp/node_modules/debug/src/index.js +0 -10
  232. gaia/eval/webapp/node_modules/debug/src/inspector-log.js +0 -15
  233. gaia/eval/webapp/node_modules/debug/src/node.js +0 -248
  234. gaia/eval/webapp/node_modules/depd/History.md +0 -103
  235. gaia/eval/webapp/node_modules/depd/LICENSE +0 -22
  236. gaia/eval/webapp/node_modules/depd/Readme.md +0 -280
  237. gaia/eval/webapp/node_modules/depd/index.js +0 -538
  238. gaia/eval/webapp/node_modules/depd/lib/browser/index.js +0 -77
  239. gaia/eval/webapp/node_modules/depd/package.json +0 -45
  240. gaia/eval/webapp/node_modules/destroy/LICENSE +0 -23
  241. gaia/eval/webapp/node_modules/destroy/README.md +0 -63
  242. gaia/eval/webapp/node_modules/destroy/index.js +0 -209
  243. gaia/eval/webapp/node_modules/destroy/package.json +0 -48
  244. gaia/eval/webapp/node_modules/dunder-proto/.eslintrc +0 -5
  245. gaia/eval/webapp/node_modules/dunder-proto/.github/FUNDING.yml +0 -12
  246. gaia/eval/webapp/node_modules/dunder-proto/.nycrc +0 -13
  247. gaia/eval/webapp/node_modules/dunder-proto/CHANGELOG.md +0 -24
  248. gaia/eval/webapp/node_modules/dunder-proto/LICENSE +0 -21
  249. gaia/eval/webapp/node_modules/dunder-proto/README.md +0 -54
  250. gaia/eval/webapp/node_modules/dunder-proto/get.d.ts +0 -5
  251. gaia/eval/webapp/node_modules/dunder-proto/get.js +0 -30
  252. gaia/eval/webapp/node_modules/dunder-proto/package.json +0 -76
  253. gaia/eval/webapp/node_modules/dunder-proto/set.d.ts +0 -5
  254. gaia/eval/webapp/node_modules/dunder-proto/set.js +0 -35
  255. gaia/eval/webapp/node_modules/dunder-proto/test/get.js +0 -34
  256. gaia/eval/webapp/node_modules/dunder-proto/test/index.js +0 -4
  257. gaia/eval/webapp/node_modules/dunder-proto/test/set.js +0 -50
  258. gaia/eval/webapp/node_modules/dunder-proto/tsconfig.json +0 -9
  259. gaia/eval/webapp/node_modules/ee-first/LICENSE +0 -22
  260. gaia/eval/webapp/node_modules/ee-first/README.md +0 -80
  261. gaia/eval/webapp/node_modules/ee-first/index.js +0 -95
  262. gaia/eval/webapp/node_modules/ee-first/package.json +0 -29
  263. gaia/eval/webapp/node_modules/encodeurl/LICENSE +0 -22
  264. gaia/eval/webapp/node_modules/encodeurl/README.md +0 -109
  265. gaia/eval/webapp/node_modules/encodeurl/index.js +0 -60
  266. gaia/eval/webapp/node_modules/encodeurl/package.json +0 -40
  267. gaia/eval/webapp/node_modules/es-define-property/.eslintrc +0 -13
  268. gaia/eval/webapp/node_modules/es-define-property/.github/FUNDING.yml +0 -12
  269. gaia/eval/webapp/node_modules/es-define-property/.nycrc +0 -9
  270. gaia/eval/webapp/node_modules/es-define-property/CHANGELOG.md +0 -29
  271. gaia/eval/webapp/node_modules/es-define-property/LICENSE +0 -21
  272. gaia/eval/webapp/node_modules/es-define-property/README.md +0 -49
  273. gaia/eval/webapp/node_modules/es-define-property/index.d.ts +0 -3
  274. gaia/eval/webapp/node_modules/es-define-property/index.js +0 -14
  275. gaia/eval/webapp/node_modules/es-define-property/package.json +0 -81
  276. gaia/eval/webapp/node_modules/es-define-property/test/index.js +0 -56
  277. gaia/eval/webapp/node_modules/es-define-property/tsconfig.json +0 -10
  278. gaia/eval/webapp/node_modules/es-errors/.eslintrc +0 -5
  279. gaia/eval/webapp/node_modules/es-errors/.github/FUNDING.yml +0 -12
  280. gaia/eval/webapp/node_modules/es-errors/CHANGELOG.md +0 -40
  281. gaia/eval/webapp/node_modules/es-errors/LICENSE +0 -21
  282. gaia/eval/webapp/node_modules/es-errors/README.md +0 -55
  283. gaia/eval/webapp/node_modules/es-errors/eval.d.ts +0 -3
  284. gaia/eval/webapp/node_modules/es-errors/eval.js +0 -4
  285. gaia/eval/webapp/node_modules/es-errors/index.d.ts +0 -3
  286. gaia/eval/webapp/node_modules/es-errors/index.js +0 -4
  287. gaia/eval/webapp/node_modules/es-errors/package.json +0 -80
  288. gaia/eval/webapp/node_modules/es-errors/range.d.ts +0 -3
  289. gaia/eval/webapp/node_modules/es-errors/range.js +0 -4
  290. gaia/eval/webapp/node_modules/es-errors/ref.d.ts +0 -3
  291. gaia/eval/webapp/node_modules/es-errors/ref.js +0 -4
  292. gaia/eval/webapp/node_modules/es-errors/syntax.d.ts +0 -3
  293. gaia/eval/webapp/node_modules/es-errors/syntax.js +0 -4
  294. gaia/eval/webapp/node_modules/es-errors/test/index.js +0 -19
  295. gaia/eval/webapp/node_modules/es-errors/tsconfig.json +0 -49
  296. gaia/eval/webapp/node_modules/es-errors/type.d.ts +0 -3
  297. gaia/eval/webapp/node_modules/es-errors/type.js +0 -4
  298. gaia/eval/webapp/node_modules/es-errors/uri.d.ts +0 -3
  299. gaia/eval/webapp/node_modules/es-errors/uri.js +0 -4
  300. gaia/eval/webapp/node_modules/es-object-atoms/.eslintrc +0 -16
  301. gaia/eval/webapp/node_modules/es-object-atoms/.github/FUNDING.yml +0 -12
  302. gaia/eval/webapp/node_modules/es-object-atoms/CHANGELOG.md +0 -37
  303. gaia/eval/webapp/node_modules/es-object-atoms/LICENSE +0 -21
  304. gaia/eval/webapp/node_modules/es-object-atoms/README.md +0 -63
  305. gaia/eval/webapp/node_modules/es-object-atoms/RequireObjectCoercible.d.ts +0 -3
  306. gaia/eval/webapp/node_modules/es-object-atoms/RequireObjectCoercible.js +0 -11
  307. gaia/eval/webapp/node_modules/es-object-atoms/ToObject.d.ts +0 -7
  308. gaia/eval/webapp/node_modules/es-object-atoms/ToObject.js +0 -10
  309. gaia/eval/webapp/node_modules/es-object-atoms/index.d.ts +0 -3
  310. gaia/eval/webapp/node_modules/es-object-atoms/index.js +0 -4
  311. gaia/eval/webapp/node_modules/es-object-atoms/isObject.d.ts +0 -3
  312. gaia/eval/webapp/node_modules/es-object-atoms/isObject.js +0 -6
  313. gaia/eval/webapp/node_modules/es-object-atoms/package.json +0 -80
  314. gaia/eval/webapp/node_modules/es-object-atoms/test/index.js +0 -38
  315. gaia/eval/webapp/node_modules/es-object-atoms/tsconfig.json +0 -6
  316. gaia/eval/webapp/node_modules/escape-html/LICENSE +0 -24
  317. gaia/eval/webapp/node_modules/escape-html/Readme.md +0 -43
  318. gaia/eval/webapp/node_modules/escape-html/index.js +0 -78
  319. gaia/eval/webapp/node_modules/escape-html/package.json +0 -24
  320. gaia/eval/webapp/node_modules/etag/HISTORY.md +0 -83
  321. gaia/eval/webapp/node_modules/etag/LICENSE +0 -22
  322. gaia/eval/webapp/node_modules/etag/README.md +0 -159
  323. gaia/eval/webapp/node_modules/etag/index.js +0 -131
  324. gaia/eval/webapp/node_modules/etag/package.json +0 -47
  325. gaia/eval/webapp/node_modules/express/History.md +0 -3656
  326. gaia/eval/webapp/node_modules/express/LICENSE +0 -24
  327. gaia/eval/webapp/node_modules/express/Readme.md +0 -260
  328. gaia/eval/webapp/node_modules/express/index.js +0 -11
  329. gaia/eval/webapp/node_modules/express/lib/application.js +0 -661
  330. gaia/eval/webapp/node_modules/express/lib/express.js +0 -116
  331. gaia/eval/webapp/node_modules/express/lib/middleware/init.js +0 -43
  332. gaia/eval/webapp/node_modules/express/lib/middleware/query.js +0 -47
  333. gaia/eval/webapp/node_modules/express/lib/request.js +0 -525
  334. gaia/eval/webapp/node_modules/express/lib/response.js +0 -1179
  335. gaia/eval/webapp/node_modules/express/lib/router/index.js +0 -673
  336. gaia/eval/webapp/node_modules/express/lib/router/layer.js +0 -181
  337. gaia/eval/webapp/node_modules/express/lib/router/route.js +0 -230
  338. gaia/eval/webapp/node_modules/express/lib/utils.js +0 -303
  339. gaia/eval/webapp/node_modules/express/lib/view.js +0 -182
  340. gaia/eval/webapp/node_modules/express/package.json +0 -102
  341. gaia/eval/webapp/node_modules/finalhandler/HISTORY.md +0 -210
  342. gaia/eval/webapp/node_modules/finalhandler/LICENSE +0 -22
  343. gaia/eval/webapp/node_modules/finalhandler/README.md +0 -147
  344. gaia/eval/webapp/node_modules/finalhandler/SECURITY.md +0 -25
  345. gaia/eval/webapp/node_modules/finalhandler/index.js +0 -341
  346. gaia/eval/webapp/node_modules/finalhandler/package.json +0 -47
  347. gaia/eval/webapp/node_modules/forwarded/HISTORY.md +0 -21
  348. gaia/eval/webapp/node_modules/forwarded/LICENSE +0 -22
  349. gaia/eval/webapp/node_modules/forwarded/README.md +0 -57
  350. gaia/eval/webapp/node_modules/forwarded/index.js +0 -90
  351. gaia/eval/webapp/node_modules/forwarded/package.json +0 -45
  352. gaia/eval/webapp/node_modules/fresh/HISTORY.md +0 -70
  353. gaia/eval/webapp/node_modules/fresh/LICENSE +0 -23
  354. gaia/eval/webapp/node_modules/fresh/README.md +0 -119
  355. gaia/eval/webapp/node_modules/fresh/index.js +0 -137
  356. gaia/eval/webapp/node_modules/fresh/package.json +0 -46
  357. gaia/eval/webapp/node_modules/fs/README.md +0 -9
  358. gaia/eval/webapp/node_modules/fs/package.json +0 -20
  359. gaia/eval/webapp/node_modules/function-bind/.eslintrc +0 -21
  360. gaia/eval/webapp/node_modules/function-bind/.github/FUNDING.yml +0 -12
  361. gaia/eval/webapp/node_modules/function-bind/.github/SECURITY.md +0 -3
  362. gaia/eval/webapp/node_modules/function-bind/.nycrc +0 -13
  363. gaia/eval/webapp/node_modules/function-bind/CHANGELOG.md +0 -136
  364. gaia/eval/webapp/node_modules/function-bind/LICENSE +0 -20
  365. gaia/eval/webapp/node_modules/function-bind/README.md +0 -46
  366. gaia/eval/webapp/node_modules/function-bind/implementation.js +0 -84
  367. gaia/eval/webapp/node_modules/function-bind/index.js +0 -5
  368. gaia/eval/webapp/node_modules/function-bind/package.json +0 -87
  369. gaia/eval/webapp/node_modules/function-bind/test/.eslintrc +0 -9
  370. gaia/eval/webapp/node_modules/function-bind/test/index.js +0 -252
  371. gaia/eval/webapp/node_modules/get-intrinsic/.eslintrc +0 -42
  372. gaia/eval/webapp/node_modules/get-intrinsic/.github/FUNDING.yml +0 -12
  373. gaia/eval/webapp/node_modules/get-intrinsic/.nycrc +0 -9
  374. gaia/eval/webapp/node_modules/get-intrinsic/CHANGELOG.md +0 -186
  375. gaia/eval/webapp/node_modules/get-intrinsic/LICENSE +0 -21
  376. gaia/eval/webapp/node_modules/get-intrinsic/README.md +0 -71
  377. gaia/eval/webapp/node_modules/get-intrinsic/index.js +0 -378
  378. gaia/eval/webapp/node_modules/get-intrinsic/package.json +0 -97
  379. gaia/eval/webapp/node_modules/get-intrinsic/test/GetIntrinsic.js +0 -274
  380. gaia/eval/webapp/node_modules/get-proto/.eslintrc +0 -10
  381. gaia/eval/webapp/node_modules/get-proto/.github/FUNDING.yml +0 -12
  382. gaia/eval/webapp/node_modules/get-proto/.nycrc +0 -9
  383. gaia/eval/webapp/node_modules/get-proto/CHANGELOG.md +0 -21
  384. gaia/eval/webapp/node_modules/get-proto/LICENSE +0 -21
  385. gaia/eval/webapp/node_modules/get-proto/Object.getPrototypeOf.d.ts +0 -5
  386. gaia/eval/webapp/node_modules/get-proto/Object.getPrototypeOf.js +0 -6
  387. gaia/eval/webapp/node_modules/get-proto/README.md +0 -50
  388. gaia/eval/webapp/node_modules/get-proto/Reflect.getPrototypeOf.d.ts +0 -3
  389. gaia/eval/webapp/node_modules/get-proto/Reflect.getPrototypeOf.js +0 -4
  390. gaia/eval/webapp/node_modules/get-proto/index.d.ts +0 -5
  391. gaia/eval/webapp/node_modules/get-proto/index.js +0 -27
  392. gaia/eval/webapp/node_modules/get-proto/package.json +0 -81
  393. gaia/eval/webapp/node_modules/get-proto/test/index.js +0 -68
  394. gaia/eval/webapp/node_modules/get-proto/tsconfig.json +0 -9
  395. gaia/eval/webapp/node_modules/gopd/.eslintrc +0 -16
  396. gaia/eval/webapp/node_modules/gopd/.github/FUNDING.yml +0 -12
  397. gaia/eval/webapp/node_modules/gopd/CHANGELOG.md +0 -45
  398. gaia/eval/webapp/node_modules/gopd/LICENSE +0 -21
  399. gaia/eval/webapp/node_modules/gopd/README.md +0 -40
  400. gaia/eval/webapp/node_modules/gopd/gOPD.d.ts +0 -1
  401. gaia/eval/webapp/node_modules/gopd/gOPD.js +0 -4
  402. gaia/eval/webapp/node_modules/gopd/index.d.ts +0 -5
  403. gaia/eval/webapp/node_modules/gopd/index.js +0 -15
  404. gaia/eval/webapp/node_modules/gopd/package.json +0 -77
  405. gaia/eval/webapp/node_modules/gopd/test/index.js +0 -36
  406. gaia/eval/webapp/node_modules/gopd/tsconfig.json +0 -9
  407. gaia/eval/webapp/node_modules/has-symbols/.eslintrc +0 -11
  408. gaia/eval/webapp/node_modules/has-symbols/.github/FUNDING.yml +0 -12
  409. gaia/eval/webapp/node_modules/has-symbols/.nycrc +0 -9
  410. gaia/eval/webapp/node_modules/has-symbols/CHANGELOG.md +0 -91
  411. gaia/eval/webapp/node_modules/has-symbols/LICENSE +0 -21
  412. gaia/eval/webapp/node_modules/has-symbols/README.md +0 -46
  413. gaia/eval/webapp/node_modules/has-symbols/index.d.ts +0 -3
  414. gaia/eval/webapp/node_modules/has-symbols/index.js +0 -14
  415. gaia/eval/webapp/node_modules/has-symbols/package.json +0 -111
  416. gaia/eval/webapp/node_modules/has-symbols/shams.d.ts +0 -3
  417. gaia/eval/webapp/node_modules/has-symbols/shams.js +0 -45
  418. gaia/eval/webapp/node_modules/has-symbols/test/index.js +0 -22
  419. gaia/eval/webapp/node_modules/has-symbols/test/shams/core-js.js +0 -29
  420. gaia/eval/webapp/node_modules/has-symbols/test/shams/get-own-property-symbols.js +0 -29
  421. gaia/eval/webapp/node_modules/has-symbols/test/tests.js +0 -58
  422. gaia/eval/webapp/node_modules/has-symbols/tsconfig.json +0 -10
  423. gaia/eval/webapp/node_modules/hasown/.eslintrc +0 -5
  424. gaia/eval/webapp/node_modules/hasown/.github/FUNDING.yml +0 -12
  425. gaia/eval/webapp/node_modules/hasown/.nycrc +0 -13
  426. gaia/eval/webapp/node_modules/hasown/CHANGELOG.md +0 -40
  427. gaia/eval/webapp/node_modules/hasown/LICENSE +0 -21
  428. gaia/eval/webapp/node_modules/hasown/README.md +0 -40
  429. gaia/eval/webapp/node_modules/hasown/index.d.ts +0 -3
  430. gaia/eval/webapp/node_modules/hasown/index.js +0 -8
  431. gaia/eval/webapp/node_modules/hasown/package.json +0 -92
  432. gaia/eval/webapp/node_modules/hasown/tsconfig.json +0 -6
  433. gaia/eval/webapp/node_modules/http-errors/HISTORY.md +0 -180
  434. gaia/eval/webapp/node_modules/http-errors/LICENSE +0 -23
  435. gaia/eval/webapp/node_modules/http-errors/README.md +0 -169
  436. gaia/eval/webapp/node_modules/http-errors/index.js +0 -289
  437. gaia/eval/webapp/node_modules/http-errors/package.json +0 -50
  438. gaia/eval/webapp/node_modules/iconv-lite/Changelog.md +0 -162
  439. gaia/eval/webapp/node_modules/iconv-lite/LICENSE +0 -21
  440. gaia/eval/webapp/node_modules/iconv-lite/README.md +0 -156
  441. gaia/eval/webapp/node_modules/iconv-lite/encodings/dbcs-codec.js +0 -555
  442. gaia/eval/webapp/node_modules/iconv-lite/encodings/dbcs-data.js +0 -176
  443. gaia/eval/webapp/node_modules/iconv-lite/encodings/index.js +0 -22
  444. gaia/eval/webapp/node_modules/iconv-lite/encodings/internal.js +0 -188
  445. gaia/eval/webapp/node_modules/iconv-lite/encodings/sbcs-codec.js +0 -72
  446. gaia/eval/webapp/node_modules/iconv-lite/encodings/sbcs-data-generated.js +0 -451
  447. gaia/eval/webapp/node_modules/iconv-lite/encodings/sbcs-data.js +0 -174
  448. gaia/eval/webapp/node_modules/iconv-lite/encodings/tables/big5-added.json +0 -122
  449. gaia/eval/webapp/node_modules/iconv-lite/encodings/tables/cp936.json +0 -264
  450. gaia/eval/webapp/node_modules/iconv-lite/encodings/tables/cp949.json +0 -273
  451. gaia/eval/webapp/node_modules/iconv-lite/encodings/tables/cp950.json +0 -177
  452. gaia/eval/webapp/node_modules/iconv-lite/encodings/tables/eucjp.json +0 -182
  453. gaia/eval/webapp/node_modules/iconv-lite/encodings/tables/gb18030-ranges.json +0 -1
  454. gaia/eval/webapp/node_modules/iconv-lite/encodings/tables/gbk-added.json +0 -55
  455. gaia/eval/webapp/node_modules/iconv-lite/encodings/tables/shiftjis.json +0 -125
  456. gaia/eval/webapp/node_modules/iconv-lite/encodings/utf16.js +0 -177
  457. gaia/eval/webapp/node_modules/iconv-lite/encodings/utf7.js +0 -290
  458. gaia/eval/webapp/node_modules/iconv-lite/lib/bom-handling.js +0 -52
  459. gaia/eval/webapp/node_modules/iconv-lite/lib/extend-node.js +0 -217
  460. gaia/eval/webapp/node_modules/iconv-lite/lib/index.d.ts +0 -24
  461. gaia/eval/webapp/node_modules/iconv-lite/lib/index.js +0 -153
  462. gaia/eval/webapp/node_modules/iconv-lite/lib/streams.js +0 -121
  463. gaia/eval/webapp/node_modules/iconv-lite/package.json +0 -46
  464. gaia/eval/webapp/node_modules/inherits/LICENSE +0 -16
  465. gaia/eval/webapp/node_modules/inherits/README.md +0 -42
  466. gaia/eval/webapp/node_modules/inherits/inherits.js +0 -9
  467. gaia/eval/webapp/node_modules/inherits/inherits_browser.js +0 -27
  468. gaia/eval/webapp/node_modules/inherits/package.json +0 -29
  469. gaia/eval/webapp/node_modules/ipaddr.js/LICENSE +0 -19
  470. gaia/eval/webapp/node_modules/ipaddr.js/README.md +0 -233
  471. gaia/eval/webapp/node_modules/ipaddr.js/ipaddr.min.js +0 -1
  472. gaia/eval/webapp/node_modules/ipaddr.js/lib/ipaddr.js +0 -673
  473. gaia/eval/webapp/node_modules/ipaddr.js/lib/ipaddr.js.d.ts +0 -68
  474. gaia/eval/webapp/node_modules/ipaddr.js/package.json +0 -35
  475. gaia/eval/webapp/node_modules/math-intrinsics/.eslintrc +0 -16
  476. gaia/eval/webapp/node_modules/math-intrinsics/.github/FUNDING.yml +0 -12
  477. gaia/eval/webapp/node_modules/math-intrinsics/CHANGELOG.md +0 -24
  478. gaia/eval/webapp/node_modules/math-intrinsics/LICENSE +0 -21
  479. gaia/eval/webapp/node_modules/math-intrinsics/README.md +0 -50
  480. gaia/eval/webapp/node_modules/math-intrinsics/abs.d.ts +0 -1
  481. gaia/eval/webapp/node_modules/math-intrinsics/abs.js +0 -4
  482. gaia/eval/webapp/node_modules/math-intrinsics/constants/maxArrayLength.d.ts +0 -3
  483. gaia/eval/webapp/node_modules/math-intrinsics/constants/maxArrayLength.js +0 -4
  484. gaia/eval/webapp/node_modules/math-intrinsics/constants/maxSafeInteger.d.ts +0 -3
  485. gaia/eval/webapp/node_modules/math-intrinsics/constants/maxSafeInteger.js +0 -5
  486. gaia/eval/webapp/node_modules/math-intrinsics/constants/maxValue.d.ts +0 -3
  487. gaia/eval/webapp/node_modules/math-intrinsics/constants/maxValue.js +0 -5
  488. gaia/eval/webapp/node_modules/math-intrinsics/floor.d.ts +0 -1
  489. gaia/eval/webapp/node_modules/math-intrinsics/floor.js +0 -4
  490. gaia/eval/webapp/node_modules/math-intrinsics/isFinite.d.ts +0 -3
  491. gaia/eval/webapp/node_modules/math-intrinsics/isFinite.js +0 -12
  492. gaia/eval/webapp/node_modules/math-intrinsics/isInteger.d.ts +0 -3
  493. gaia/eval/webapp/node_modules/math-intrinsics/isInteger.js +0 -16
  494. gaia/eval/webapp/node_modules/math-intrinsics/isNaN.d.ts +0 -1
  495. gaia/eval/webapp/node_modules/math-intrinsics/isNaN.js +0 -6
  496. gaia/eval/webapp/node_modules/math-intrinsics/isNegativeZero.d.ts +0 -3
  497. gaia/eval/webapp/node_modules/math-intrinsics/isNegativeZero.js +0 -6
  498. gaia/eval/webapp/node_modules/math-intrinsics/max.d.ts +0 -1
  499. gaia/eval/webapp/node_modules/math-intrinsics/max.js +0 -4
  500. gaia/eval/webapp/node_modules/math-intrinsics/min.d.ts +0 -1
  501. gaia/eval/webapp/node_modules/math-intrinsics/min.js +0 -4
  502. gaia/eval/webapp/node_modules/math-intrinsics/mod.d.ts +0 -3
  503. gaia/eval/webapp/node_modules/math-intrinsics/mod.js +0 -9
  504. gaia/eval/webapp/node_modules/math-intrinsics/package.json +0 -86
  505. gaia/eval/webapp/node_modules/math-intrinsics/pow.d.ts +0 -1
  506. gaia/eval/webapp/node_modules/math-intrinsics/pow.js +0 -4
  507. gaia/eval/webapp/node_modules/math-intrinsics/round.d.ts +0 -1
  508. gaia/eval/webapp/node_modules/math-intrinsics/round.js +0 -4
  509. gaia/eval/webapp/node_modules/math-intrinsics/sign.d.ts +0 -3
  510. gaia/eval/webapp/node_modules/math-intrinsics/sign.js +0 -11
  511. gaia/eval/webapp/node_modules/math-intrinsics/test/index.js +0 -192
  512. gaia/eval/webapp/node_modules/math-intrinsics/tsconfig.json +0 -3
  513. gaia/eval/webapp/node_modules/media-typer/HISTORY.md +0 -22
  514. gaia/eval/webapp/node_modules/media-typer/LICENSE +0 -22
  515. gaia/eval/webapp/node_modules/media-typer/README.md +0 -81
  516. gaia/eval/webapp/node_modules/media-typer/index.js +0 -270
  517. gaia/eval/webapp/node_modules/media-typer/package.json +0 -26
  518. gaia/eval/webapp/node_modules/merge-descriptors/HISTORY.md +0 -21
  519. gaia/eval/webapp/node_modules/merge-descriptors/LICENSE +0 -23
  520. gaia/eval/webapp/node_modules/merge-descriptors/README.md +0 -49
  521. gaia/eval/webapp/node_modules/merge-descriptors/index.js +0 -60
  522. gaia/eval/webapp/node_modules/merge-descriptors/package.json +0 -39
  523. gaia/eval/webapp/node_modules/methods/HISTORY.md +0 -29
  524. gaia/eval/webapp/node_modules/methods/LICENSE +0 -24
  525. gaia/eval/webapp/node_modules/methods/README.md +0 -51
  526. gaia/eval/webapp/node_modules/methods/index.js +0 -69
  527. gaia/eval/webapp/node_modules/methods/package.json +0 -36
  528. gaia/eval/webapp/node_modules/mime/.npmignore +0 -0
  529. gaia/eval/webapp/node_modules/mime/CHANGELOG.md +0 -164
  530. gaia/eval/webapp/node_modules/mime/LICENSE +0 -21
  531. gaia/eval/webapp/node_modules/mime/README.md +0 -90
  532. gaia/eval/webapp/node_modules/mime/cli.js +0 -8
  533. gaia/eval/webapp/node_modules/mime/mime.js +0 -108
  534. gaia/eval/webapp/node_modules/mime/package.json +0 -44
  535. gaia/eval/webapp/node_modules/mime/src/build.js +0 -53
  536. gaia/eval/webapp/node_modules/mime/src/test.js +0 -60
  537. gaia/eval/webapp/node_modules/mime/types.json +0 -1
  538. gaia/eval/webapp/node_modules/mime-db/HISTORY.md +0 -507
  539. gaia/eval/webapp/node_modules/mime-db/LICENSE +0 -23
  540. gaia/eval/webapp/node_modules/mime-db/README.md +0 -100
  541. gaia/eval/webapp/node_modules/mime-db/db.json +0 -8519
  542. gaia/eval/webapp/node_modules/mime-db/index.js +0 -12
  543. gaia/eval/webapp/node_modules/mime-db/package.json +0 -60
  544. gaia/eval/webapp/node_modules/mime-types/HISTORY.md +0 -397
  545. gaia/eval/webapp/node_modules/mime-types/LICENSE +0 -23
  546. gaia/eval/webapp/node_modules/mime-types/README.md +0 -113
  547. gaia/eval/webapp/node_modules/mime-types/index.js +0 -188
  548. gaia/eval/webapp/node_modules/mime-types/package.json +0 -44
  549. gaia/eval/webapp/node_modules/ms/index.js +0 -152
  550. gaia/eval/webapp/node_modules/ms/license.md +0 -21
  551. gaia/eval/webapp/node_modules/ms/package.json +0 -37
  552. gaia/eval/webapp/node_modules/ms/readme.md +0 -51
  553. gaia/eval/webapp/node_modules/negotiator/HISTORY.md +0 -108
  554. gaia/eval/webapp/node_modules/negotiator/LICENSE +0 -24
  555. gaia/eval/webapp/node_modules/negotiator/README.md +0 -203
  556. gaia/eval/webapp/node_modules/negotiator/index.js +0 -82
  557. gaia/eval/webapp/node_modules/negotiator/lib/charset.js +0 -169
  558. gaia/eval/webapp/node_modules/negotiator/lib/encoding.js +0 -184
  559. gaia/eval/webapp/node_modules/negotiator/lib/language.js +0 -179
  560. gaia/eval/webapp/node_modules/negotiator/lib/mediaType.js +0 -294
  561. gaia/eval/webapp/node_modules/negotiator/package.json +0 -42
  562. gaia/eval/webapp/node_modules/object-inspect/.eslintrc +0 -53
  563. gaia/eval/webapp/node_modules/object-inspect/.github/FUNDING.yml +0 -12
  564. gaia/eval/webapp/node_modules/object-inspect/.nycrc +0 -13
  565. gaia/eval/webapp/node_modules/object-inspect/CHANGELOG.md +0 -424
  566. gaia/eval/webapp/node_modules/object-inspect/LICENSE +0 -21
  567. gaia/eval/webapp/node_modules/object-inspect/example/all.js +0 -23
  568. gaia/eval/webapp/node_modules/object-inspect/example/circular.js +0 -6
  569. gaia/eval/webapp/node_modules/object-inspect/example/fn.js +0 -5
  570. gaia/eval/webapp/node_modules/object-inspect/example/inspect.js +0 -10
  571. gaia/eval/webapp/node_modules/object-inspect/index.js +0 -544
  572. gaia/eval/webapp/node_modules/object-inspect/package-support.json +0 -20
  573. gaia/eval/webapp/node_modules/object-inspect/package.json +0 -105
  574. gaia/eval/webapp/node_modules/object-inspect/readme.markdown +0 -84
  575. gaia/eval/webapp/node_modules/object-inspect/test/bigint.js +0 -58
  576. gaia/eval/webapp/node_modules/object-inspect/test/browser/dom.js +0 -15
  577. gaia/eval/webapp/node_modules/object-inspect/test/circular.js +0 -16
  578. gaia/eval/webapp/node_modules/object-inspect/test/deep.js +0 -12
  579. gaia/eval/webapp/node_modules/object-inspect/test/element.js +0 -53
  580. gaia/eval/webapp/node_modules/object-inspect/test/err.js +0 -48
  581. gaia/eval/webapp/node_modules/object-inspect/test/fakes.js +0 -29
  582. gaia/eval/webapp/node_modules/object-inspect/test/fn.js +0 -76
  583. gaia/eval/webapp/node_modules/object-inspect/test/global.js +0 -17
  584. gaia/eval/webapp/node_modules/object-inspect/test/has.js +0 -15
  585. gaia/eval/webapp/node_modules/object-inspect/test/holes.js +0 -15
  586. gaia/eval/webapp/node_modules/object-inspect/test/indent-option.js +0 -271
  587. gaia/eval/webapp/node_modules/object-inspect/test/inspect.js +0 -139
  588. gaia/eval/webapp/node_modules/object-inspect/test/lowbyte.js +0 -12
  589. gaia/eval/webapp/node_modules/object-inspect/test/number.js +0 -58
  590. gaia/eval/webapp/node_modules/object-inspect/test/quoteStyle.js +0 -26
  591. gaia/eval/webapp/node_modules/object-inspect/test/toStringTag.js +0 -40
  592. gaia/eval/webapp/node_modules/object-inspect/test/undef.js +0 -12
  593. gaia/eval/webapp/node_modules/object-inspect/test/values.js +0 -261
  594. gaia/eval/webapp/node_modules/object-inspect/test-core-js.js +0 -26
  595. gaia/eval/webapp/node_modules/object-inspect/util.inspect.js +0 -1
  596. gaia/eval/webapp/node_modules/on-finished/HISTORY.md +0 -98
  597. gaia/eval/webapp/node_modules/on-finished/LICENSE +0 -23
  598. gaia/eval/webapp/node_modules/on-finished/README.md +0 -162
  599. gaia/eval/webapp/node_modules/on-finished/index.js +0 -234
  600. gaia/eval/webapp/node_modules/on-finished/package.json +0 -39
  601. gaia/eval/webapp/node_modules/parseurl/HISTORY.md +0 -58
  602. gaia/eval/webapp/node_modules/parseurl/LICENSE +0 -24
  603. gaia/eval/webapp/node_modules/parseurl/README.md +0 -133
  604. gaia/eval/webapp/node_modules/parseurl/index.js +0 -158
  605. gaia/eval/webapp/node_modules/parseurl/package.json +0 -40
  606. gaia/eval/webapp/node_modules/path/.npmignore +0 -1
  607. gaia/eval/webapp/node_modules/path/LICENSE +0 -18
  608. gaia/eval/webapp/node_modules/path/README.md +0 -15
  609. gaia/eval/webapp/node_modules/path/package.json +0 -24
  610. gaia/eval/webapp/node_modules/path/path.js +0 -628
  611. gaia/eval/webapp/node_modules/path-to-regexp/LICENSE +0 -21
  612. gaia/eval/webapp/node_modules/path-to-regexp/Readme.md +0 -35
  613. gaia/eval/webapp/node_modules/path-to-regexp/index.js +0 -156
  614. gaia/eval/webapp/node_modules/path-to-regexp/package.json +0 -30
  615. gaia/eval/webapp/node_modules/process/.eslintrc +0 -21
  616. gaia/eval/webapp/node_modules/process/LICENSE +0 -22
  617. gaia/eval/webapp/node_modules/process/README.md +0 -26
  618. gaia/eval/webapp/node_modules/process/browser.js +0 -184
  619. gaia/eval/webapp/node_modules/process/index.js +0 -2
  620. gaia/eval/webapp/node_modules/process/package.json +0 -27
  621. gaia/eval/webapp/node_modules/process/test.js +0 -199
  622. gaia/eval/webapp/node_modules/proxy-addr/HISTORY.md +0 -161
  623. gaia/eval/webapp/node_modules/proxy-addr/LICENSE +0 -22
  624. gaia/eval/webapp/node_modules/proxy-addr/README.md +0 -139
  625. gaia/eval/webapp/node_modules/proxy-addr/index.js +0 -327
  626. gaia/eval/webapp/node_modules/proxy-addr/package.json +0 -47
  627. gaia/eval/webapp/node_modules/qs/.editorconfig +0 -46
  628. gaia/eval/webapp/node_modules/qs/.eslintrc +0 -38
  629. gaia/eval/webapp/node_modules/qs/.github/FUNDING.yml +0 -12
  630. gaia/eval/webapp/node_modules/qs/.nycrc +0 -13
  631. gaia/eval/webapp/node_modules/qs/CHANGELOG.md +0 -600
  632. gaia/eval/webapp/node_modules/qs/LICENSE.md +0 -29
  633. gaia/eval/webapp/node_modules/qs/README.md +0 -709
  634. gaia/eval/webapp/node_modules/qs/dist/qs.js +0 -90
  635. gaia/eval/webapp/node_modules/qs/lib/formats.js +0 -23
  636. gaia/eval/webapp/node_modules/qs/lib/index.js +0 -11
  637. gaia/eval/webapp/node_modules/qs/lib/parse.js +0 -296
  638. gaia/eval/webapp/node_modules/qs/lib/stringify.js +0 -351
  639. gaia/eval/webapp/node_modules/qs/lib/utils.js +0 -265
  640. gaia/eval/webapp/node_modules/qs/package.json +0 -91
  641. gaia/eval/webapp/node_modules/qs/test/empty-keys-cases.js +0 -267
  642. gaia/eval/webapp/node_modules/qs/test/parse.js +0 -1170
  643. gaia/eval/webapp/node_modules/qs/test/stringify.js +0 -1298
  644. gaia/eval/webapp/node_modules/qs/test/utils.js +0 -136
  645. gaia/eval/webapp/node_modules/range-parser/HISTORY.md +0 -56
  646. gaia/eval/webapp/node_modules/range-parser/LICENSE +0 -23
  647. gaia/eval/webapp/node_modules/range-parser/README.md +0 -84
  648. gaia/eval/webapp/node_modules/range-parser/index.js +0 -162
  649. gaia/eval/webapp/node_modules/range-parser/package.json +0 -44
  650. gaia/eval/webapp/node_modules/raw-body/HISTORY.md +0 -308
  651. gaia/eval/webapp/node_modules/raw-body/LICENSE +0 -22
  652. gaia/eval/webapp/node_modules/raw-body/README.md +0 -223
  653. gaia/eval/webapp/node_modules/raw-body/SECURITY.md +0 -24
  654. gaia/eval/webapp/node_modules/raw-body/index.d.ts +0 -87
  655. gaia/eval/webapp/node_modules/raw-body/index.js +0 -336
  656. gaia/eval/webapp/node_modules/raw-body/package.json +0 -49
  657. gaia/eval/webapp/node_modules/safe-buffer/LICENSE +0 -21
  658. gaia/eval/webapp/node_modules/safe-buffer/README.md +0 -584
  659. gaia/eval/webapp/node_modules/safe-buffer/index.d.ts +0 -187
  660. gaia/eval/webapp/node_modules/safe-buffer/index.js +0 -65
  661. gaia/eval/webapp/node_modules/safe-buffer/package.json +0 -51
  662. gaia/eval/webapp/node_modules/safer-buffer/LICENSE +0 -21
  663. gaia/eval/webapp/node_modules/safer-buffer/Porting-Buffer.md +0 -268
  664. gaia/eval/webapp/node_modules/safer-buffer/Readme.md +0 -156
  665. gaia/eval/webapp/node_modules/safer-buffer/dangerous.js +0 -58
  666. gaia/eval/webapp/node_modules/safer-buffer/package.json +0 -34
  667. gaia/eval/webapp/node_modules/safer-buffer/safer.js +0 -77
  668. gaia/eval/webapp/node_modules/safer-buffer/tests.js +0 -406
  669. gaia/eval/webapp/node_modules/send/HISTORY.md +0 -526
  670. gaia/eval/webapp/node_modules/send/LICENSE +0 -23
  671. gaia/eval/webapp/node_modules/send/README.md +0 -327
  672. gaia/eval/webapp/node_modules/send/SECURITY.md +0 -24
  673. gaia/eval/webapp/node_modules/send/index.js +0 -1142
  674. gaia/eval/webapp/node_modules/send/node_modules/encodeurl/HISTORY.md +0 -14
  675. gaia/eval/webapp/node_modules/send/node_modules/encodeurl/LICENSE +0 -22
  676. gaia/eval/webapp/node_modules/send/node_modules/encodeurl/README.md +0 -128
  677. gaia/eval/webapp/node_modules/send/node_modules/encodeurl/index.js +0 -60
  678. gaia/eval/webapp/node_modules/send/node_modules/encodeurl/package.json +0 -40
  679. gaia/eval/webapp/node_modules/send/node_modules/ms/index.js +0 -162
  680. gaia/eval/webapp/node_modules/send/node_modules/ms/license.md +0 -21
  681. gaia/eval/webapp/node_modules/send/node_modules/ms/package.json +0 -38
  682. gaia/eval/webapp/node_modules/send/node_modules/ms/readme.md +0 -59
  683. gaia/eval/webapp/node_modules/send/package.json +0 -62
  684. gaia/eval/webapp/node_modules/serve-static/HISTORY.md +0 -487
  685. gaia/eval/webapp/node_modules/serve-static/LICENSE +0 -25
  686. gaia/eval/webapp/node_modules/serve-static/README.md +0 -257
  687. gaia/eval/webapp/node_modules/serve-static/index.js +0 -209
  688. gaia/eval/webapp/node_modules/serve-static/package.json +0 -42
  689. gaia/eval/webapp/node_modules/setprototypeof/LICENSE +0 -13
  690. gaia/eval/webapp/node_modules/setprototypeof/README.md +0 -31
  691. gaia/eval/webapp/node_modules/setprototypeof/index.d.ts +0 -2
  692. gaia/eval/webapp/node_modules/setprototypeof/index.js +0 -17
  693. gaia/eval/webapp/node_modules/setprototypeof/package.json +0 -38
  694. gaia/eval/webapp/node_modules/setprototypeof/test/index.js +0 -24
  695. gaia/eval/webapp/node_modules/side-channel/.editorconfig +0 -9
  696. gaia/eval/webapp/node_modules/side-channel/.eslintrc +0 -12
  697. gaia/eval/webapp/node_modules/side-channel/.github/FUNDING.yml +0 -12
  698. gaia/eval/webapp/node_modules/side-channel/.nycrc +0 -13
  699. gaia/eval/webapp/node_modules/side-channel/CHANGELOG.md +0 -110
  700. gaia/eval/webapp/node_modules/side-channel/LICENSE +0 -21
  701. gaia/eval/webapp/node_modules/side-channel/README.md +0 -61
  702. gaia/eval/webapp/node_modules/side-channel/index.d.ts +0 -14
  703. gaia/eval/webapp/node_modules/side-channel/index.js +0 -43
  704. gaia/eval/webapp/node_modules/side-channel/package.json +0 -85
  705. gaia/eval/webapp/node_modules/side-channel/test/index.js +0 -104
  706. gaia/eval/webapp/node_modules/side-channel/tsconfig.json +0 -9
  707. gaia/eval/webapp/node_modules/side-channel-list/.editorconfig +0 -9
  708. gaia/eval/webapp/node_modules/side-channel-list/.eslintrc +0 -11
  709. gaia/eval/webapp/node_modules/side-channel-list/.github/FUNDING.yml +0 -12
  710. gaia/eval/webapp/node_modules/side-channel-list/.nycrc +0 -13
  711. gaia/eval/webapp/node_modules/side-channel-list/CHANGELOG.md +0 -15
  712. gaia/eval/webapp/node_modules/side-channel-list/LICENSE +0 -21
  713. gaia/eval/webapp/node_modules/side-channel-list/README.md +0 -62
  714. gaia/eval/webapp/node_modules/side-channel-list/index.d.ts +0 -13
  715. gaia/eval/webapp/node_modules/side-channel-list/index.js +0 -113
  716. gaia/eval/webapp/node_modules/side-channel-list/list.d.ts +0 -14
  717. gaia/eval/webapp/node_modules/side-channel-list/package.json +0 -77
  718. gaia/eval/webapp/node_modules/side-channel-list/test/index.js +0 -104
  719. gaia/eval/webapp/node_modules/side-channel-list/tsconfig.json +0 -9
  720. gaia/eval/webapp/node_modules/side-channel-map/.editorconfig +0 -9
  721. gaia/eval/webapp/node_modules/side-channel-map/.eslintrc +0 -11
  722. gaia/eval/webapp/node_modules/side-channel-map/.github/FUNDING.yml +0 -12
  723. gaia/eval/webapp/node_modules/side-channel-map/.nycrc +0 -13
  724. gaia/eval/webapp/node_modules/side-channel-map/CHANGELOG.md +0 -22
  725. gaia/eval/webapp/node_modules/side-channel-map/LICENSE +0 -21
  726. gaia/eval/webapp/node_modules/side-channel-map/README.md +0 -62
  727. gaia/eval/webapp/node_modules/side-channel-map/index.d.ts +0 -15
  728. gaia/eval/webapp/node_modules/side-channel-map/index.js +0 -68
  729. gaia/eval/webapp/node_modules/side-channel-map/package.json +0 -80
  730. gaia/eval/webapp/node_modules/side-channel-map/test/index.js +0 -114
  731. gaia/eval/webapp/node_modules/side-channel-map/tsconfig.json +0 -9
  732. gaia/eval/webapp/node_modules/side-channel-weakmap/.editorconfig +0 -9
  733. gaia/eval/webapp/node_modules/side-channel-weakmap/.eslintrc +0 -12
  734. gaia/eval/webapp/node_modules/side-channel-weakmap/.github/FUNDING.yml +0 -12
  735. gaia/eval/webapp/node_modules/side-channel-weakmap/.nycrc +0 -13
  736. gaia/eval/webapp/node_modules/side-channel-weakmap/CHANGELOG.md +0 -28
  737. gaia/eval/webapp/node_modules/side-channel-weakmap/LICENSE +0 -21
  738. gaia/eval/webapp/node_modules/side-channel-weakmap/README.md +0 -62
  739. gaia/eval/webapp/node_modules/side-channel-weakmap/index.d.ts +0 -15
  740. gaia/eval/webapp/node_modules/side-channel-weakmap/index.js +0 -84
  741. gaia/eval/webapp/node_modules/side-channel-weakmap/package.json +0 -87
  742. gaia/eval/webapp/node_modules/side-channel-weakmap/test/index.js +0 -114
  743. gaia/eval/webapp/node_modules/side-channel-weakmap/tsconfig.json +0 -9
  744. gaia/eval/webapp/node_modules/statuses/HISTORY.md +0 -82
  745. gaia/eval/webapp/node_modules/statuses/LICENSE +0 -23
  746. gaia/eval/webapp/node_modules/statuses/README.md +0 -136
  747. gaia/eval/webapp/node_modules/statuses/codes.json +0 -65
  748. gaia/eval/webapp/node_modules/statuses/index.js +0 -146
  749. gaia/eval/webapp/node_modules/statuses/package.json +0 -49
  750. gaia/eval/webapp/node_modules/toidentifier/HISTORY.md +0 -9
  751. gaia/eval/webapp/node_modules/toidentifier/LICENSE +0 -21
  752. gaia/eval/webapp/node_modules/toidentifier/README.md +0 -61
  753. gaia/eval/webapp/node_modules/toidentifier/index.js +0 -32
  754. gaia/eval/webapp/node_modules/toidentifier/package.json +0 -38
  755. gaia/eval/webapp/node_modules/type-is/HISTORY.md +0 -259
  756. gaia/eval/webapp/node_modules/type-is/LICENSE +0 -23
  757. gaia/eval/webapp/node_modules/type-is/README.md +0 -170
  758. gaia/eval/webapp/node_modules/type-is/index.js +0 -266
  759. gaia/eval/webapp/node_modules/type-is/package.json +0 -45
  760. gaia/eval/webapp/node_modules/unpipe/HISTORY.md +0 -4
  761. gaia/eval/webapp/node_modules/unpipe/LICENSE +0 -22
  762. gaia/eval/webapp/node_modules/unpipe/README.md +0 -43
  763. gaia/eval/webapp/node_modules/unpipe/index.js +0 -69
  764. gaia/eval/webapp/node_modules/unpipe/package.json +0 -27
  765. gaia/eval/webapp/node_modules/util/LICENSE +0 -18
  766. gaia/eval/webapp/node_modules/util/README.md +0 -15
  767. gaia/eval/webapp/node_modules/util/node_modules/inherits/LICENSE +0 -16
  768. gaia/eval/webapp/node_modules/util/node_modules/inherits/README.md +0 -42
  769. gaia/eval/webapp/node_modules/util/node_modules/inherits/inherits.js +0 -7
  770. gaia/eval/webapp/node_modules/util/node_modules/inherits/inherits_browser.js +0 -23
  771. gaia/eval/webapp/node_modules/util/node_modules/inherits/package.json +0 -29
  772. gaia/eval/webapp/node_modules/util/package.json +0 -35
  773. gaia/eval/webapp/node_modules/util/support/isBuffer.js +0 -3
  774. gaia/eval/webapp/node_modules/util/support/isBufferBrowser.js +0 -6
  775. gaia/eval/webapp/node_modules/util/util.js +0 -586
  776. gaia/eval/webapp/node_modules/utils-merge/.npmignore +0 -9
  777. gaia/eval/webapp/node_modules/utils-merge/LICENSE +0 -20
  778. gaia/eval/webapp/node_modules/utils-merge/README.md +0 -34
  779. gaia/eval/webapp/node_modules/utils-merge/index.js +0 -23
  780. gaia/eval/webapp/node_modules/utils-merge/package.json +0 -40
  781. gaia/eval/webapp/node_modules/vary/HISTORY.md +0 -39
  782. gaia/eval/webapp/node_modules/vary/LICENSE +0 -22
  783. gaia/eval/webapp/node_modules/vary/README.md +0 -101
  784. gaia/eval/webapp/node_modules/vary/index.js +0 -149
  785. gaia/eval/webapp/node_modules/vary/package.json +0 -43
  786. {amd_gaia-0.14.2.dist-info → amd_gaia-0.14.3.dist-info}/WHEEL +0 -0
  787. {amd_gaia-0.14.2.dist-info → amd_gaia-0.14.3.dist-info}/licenses/LICENSE.md +0 -0
  788. {amd_gaia-0.14.2.dist-info → amd_gaia-0.14.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,1506 @@
1
+ # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """
5
+ Medical Intake Agent for processing patient intake forms.
6
+
7
+ Watches a directory for new intake forms (images/PDFs), extracts patient
8
+ data using VLM, and stores records in a SQLite database.
9
+
10
+ NOTE: This is a demonstration/proof-of-concept application.
11
+ Not intended for production use with real patient data.
12
+ """
13
+
14
+ import json
15
+ import logging
16
+ import time
17
+ from pathlib import Path
18
+ from typing import Any, Dict, List, Optional
19
+
20
+ from gaia.agents.base import Agent
21
+ from gaia.agents.base.tools import tool
22
+ from gaia.database import DatabaseMixin
23
+ from gaia.llm.vlm_client import detect_image_mime_type
24
+ from gaia.utils import (
25
+ FileWatcherMixin,
26
+ compute_file_hash,
27
+ detect_field_changes,
28
+ extract_json_from_text,
29
+ pdf_page_to_image,
30
+ )
31
+
32
+ from .constants import (
33
+ EXTRACTION_PROMPT,
34
+ PATIENT_SCHEMA,
35
+ STANDARD_COLUMNS,
36
+ UPDATABLE_COLUMNS,
37
+ estimate_manual_entry_time,
38
+ )
39
+
40
+ logger = logging.getLogger(__name__)
41
+
42
+
43
+ class MedicalIntakeAgent(Agent, DatabaseMixin, FileWatcherMixin):
44
+ """
45
+ Agent for processing medical intake forms automatically.
46
+
47
+ Watches a directory for new intake forms (images/PDFs), extracts
48
+ patient data using VLM (Vision Language Model), and stores the
49
+ records in a SQLite database.
50
+
51
+ Features:
52
+ - Automatic file watching for new intake forms
53
+ - VLM-powered data extraction from images
54
+ - SQLite database storage with full-text search
55
+ - Tools for patient lookup and management
56
+ - Rich console output for processing status
57
+
58
+ Example:
59
+ from gaia.agents.emr import MedicalIntakeAgent
60
+
61
+ agent = MedicalIntakeAgent(
62
+ watch_dir="./intake_forms",
63
+ db_path="./data/patients.db",
64
+ )
65
+
66
+ # Agent automatically processes new files in watch_dir
67
+ # Query the agent about patients
68
+ agent.process_query("How many patients were processed today?")
69
+ agent.process_query("Find patient John Smith")
70
+
71
+ # Cleanup
72
+ agent.stop()
73
+ """
74
+
75
+ def __init__(
76
+ self,
77
+ watch_dir: str = "./intake_forms",
78
+ db_path: str = "./data/patients.db",
79
+ vlm_model: str = "Qwen3-VL-4B-Instruct-GGUF",
80
+ auto_start_watching: bool = True,
81
+ **kwargs,
82
+ ):
83
+ """
84
+ Initialize the Medical Intake Agent.
85
+
86
+ Args:
87
+ watch_dir: Directory to watch for new intake forms
88
+ db_path: Path to SQLite database for patient records
89
+ vlm_model: VLM model to use for extraction
90
+ auto_start_watching: Start watching immediately (default: True)
91
+ **kwargs: Additional arguments for Agent base class
92
+ """
93
+ # Set attributes before super().__init__() as it may call _get_system_prompt()
94
+ self._watch_dir = Path(watch_dir)
95
+ self._db_path = db_path
96
+ self._vlm_model = vlm_model
97
+ self._vlm = None
98
+ self._processed_files: List[Dict[str, Any]] = []
99
+ self._auto_start_watching = auto_start_watching
100
+
101
+ # Statistics
102
+ self._stats = {
103
+ "files_processed": 0,
104
+ "extraction_success": 0,
105
+ "extraction_failed": 0,
106
+ "new_patients": 0,
107
+ "returning_patients": 0,
108
+ "total_processing_time_seconds": 0.0,
109
+ "total_estimated_manual_seconds": 0.0,
110
+ "start_time": time.time(),
111
+ }
112
+
113
+ # Progress callback for external monitoring (e.g., dashboard SSE)
114
+ # Signature: callback(filename, step_num, total_steps, step_name, status)
115
+ self._progress_callback: Optional[callable] = None
116
+
117
+ # Set reasonable defaults for agent - higher max_steps for interactive use
118
+ kwargs.setdefault("max_steps", 50)
119
+
120
+ super().__init__(**kwargs)
121
+
122
+ # Initialize database
123
+ self._init_database()
124
+
125
+ # Load historical stats from database (for pre-processed forms)
126
+ self._load_historical_stats()
127
+
128
+ # Create watch directory if needed
129
+ self._watch_dir.mkdir(parents=True, exist_ok=True)
130
+
131
+ # Start file watching if requested
132
+ if auto_start_watching:
133
+ self._start_file_watching()
134
+
135
+ def _init_database(self) -> None:
136
+ """Initialize the patient database."""
137
+ try:
138
+ # Ensure data directory exists
139
+ db_dir = Path(self._db_path).parent
140
+ db_dir.mkdir(parents=True, exist_ok=True)
141
+
142
+ # Initialize database with schema
143
+ self.init_db(self._db_path)
144
+ self.execute(PATIENT_SCHEMA)
145
+ logger.info(f"Database initialized: {self._db_path}")
146
+ except Exception as e:
147
+ logger.error(f"Failed to initialize database: {e}")
148
+ raise
149
+
150
+ def _load_historical_stats(self) -> None:
151
+ """Load historical processing stats from database for pre-processed forms.
152
+
153
+ This ensures efficiency metrics include forms processed in previous sessions,
154
+ not just the current agent instance.
155
+ """
156
+ try:
157
+ # Get aggregate stats from patients table
158
+ result = self.query(
159
+ """
160
+ SELECT
161
+ COUNT(*) as total_patients,
162
+ COALESCE(SUM(processing_time_seconds), 0) as total_processing_time,
163
+ COALESCE(SUM(estimated_manual_seconds), 0) as total_estimated_manual,
164
+ SUM(CASE WHEN is_new_patient = 1 THEN 1 ELSE 0 END) as new_patients,
165
+ SUM(CASE WHEN is_new_patient = 0 THEN 1 ELSE 0 END) as returning_patients
166
+ FROM patients
167
+ """
168
+ )
169
+
170
+ if result and result[0]:
171
+ stats = result[0]
172
+ self._stats["extraction_success"] = stats.get("total_patients", 0) or 0
173
+ self._stats["files_processed"] = stats.get("total_patients", 0) or 0
174
+ self._stats["total_processing_time_seconds"] = float(
175
+ stats.get("total_processing_time", 0) or 0
176
+ )
177
+ self._stats["total_estimated_manual_seconds"] = float(
178
+ stats.get("total_estimated_manual", 0) or 0
179
+ )
180
+ self._stats["new_patients"] = stats.get("new_patients", 0) or 0
181
+ self._stats["returning_patients"] = (
182
+ stats.get("returning_patients", 0) or 0
183
+ )
184
+
185
+ if self._stats["extraction_success"] > 0:
186
+ logger.info(
187
+ f"Loaded historical stats: {self._stats['extraction_success']} forms, "
188
+ f"{self._stats['total_processing_time_seconds']:.1f}s AI time, "
189
+ f"{self._stats['total_estimated_manual_seconds']:.1f}s manual time"
190
+ )
191
+ except Exception as e:
192
+ # Don't fail if historical stats can't be loaded (e.g., schema mismatch)
193
+ logger.warning(f"Could not load historical stats: {e}")
194
+
195
+ def _start_file_watching(self) -> None:
196
+ """Start watching the intake directory for new files."""
197
+ # First, process any existing files (works even if watcher fails)
198
+ self._process_existing_files()
199
+
200
+ # Then set up the watcher for new files
201
+ try:
202
+ self.watch_directory(
203
+ self._watch_dir,
204
+ on_created=self._on_file_created,
205
+ on_modified=self._on_file_modified,
206
+ extensions=[".png", ".jpg", ".jpeg", ".pdf", ".tiff", ".bmp"],
207
+ debounce_seconds=2.0,
208
+ )
209
+ logger.info(f"Watching for intake forms: {self._watch_dir}")
210
+ except Exception as e:
211
+ logger.warning(f"File watching not available: {e}")
212
+
213
+ def _print_file_listing(
214
+ self, files: list, processed_hashes: set
215
+ ) -> tuple[int, int]:
216
+ """Print a styled listing of files in the watch directory.
217
+
218
+ Returns:
219
+ Tuple of (new_count, processed_count)
220
+ """
221
+ from rich.console import Console
222
+ from rich.table import Table
223
+
224
+ console = Console()
225
+
226
+ table = Table(
227
+ title=f"📁 {self._watch_dir}", show_header=True, header_style="bold cyan"
228
+ )
229
+ table.add_column("File", style="white")
230
+ table.add_column("Size", justify="right", style="dim")
231
+ table.add_column("Hash", style="dim")
232
+ table.add_column("Status", justify="center")
233
+
234
+ new_count = 0
235
+ processed_count = 0
236
+
237
+ for f in sorted(files):
238
+ try:
239
+ size = f.stat().st_size
240
+ if size < 1024:
241
+ size_str = f"{size} B"
242
+ elif size < 1024 * 1024:
243
+ size_str = f"{size / 1024:.1f} KB"
244
+ else:
245
+ size_str = f"{size / (1024 * 1024):.1f} MB"
246
+ except OSError:
247
+ size_str = "?"
248
+
249
+ # Compute hash for status check
250
+ file_hash = compute_file_hash(f)
251
+ hash_display = file_hash[:8] + "..." if file_hash else "?"
252
+
253
+ if file_hash and file_hash in processed_hashes:
254
+ status = "[dim]✓ processed[/dim]"
255
+ processed_count += 1
256
+ else:
257
+ status = "[green]● new[/green]"
258
+ new_count += 1
259
+
260
+ table.add_row(f.name, size_str, hash_display, status)
261
+
262
+ console.print(table)
263
+
264
+ # Print summary
265
+ summary_parts = []
266
+ if new_count > 0:
267
+ summary_parts.append(f"[green]{new_count} new[/green]")
268
+ if processed_count > 0:
269
+ summary_parts.append(f"[dim]{processed_count} already processed[/dim]")
270
+ if summary_parts:
271
+ console.print(f" {', '.join(summary_parts)}")
272
+ console.print()
273
+
274
+ return new_count, processed_count
275
+
276
+ def _process_existing_files(self) -> None:
277
+ """Scan and process any existing files in the watch directory."""
278
+ supported_extensions = {".png", ".jpg", ".jpeg", ".pdf", ".tiff", ".bmp"}
279
+
280
+ # Check directory exists
281
+ if not self._watch_dir.exists():
282
+ self.console.print_warning(
283
+ f"Watch directory does not exist: {self._watch_dir}"
284
+ )
285
+ return
286
+
287
+ # Use case-insensitive matching on Windows
288
+ existing_files = set()
289
+ try:
290
+ for f in self._watch_dir.iterdir():
291
+ if f.is_file() and f.suffix.lower() in supported_extensions:
292
+ existing_files.add(f.absolute())
293
+ except Exception as e:
294
+ self.console.print_error(f"Could not scan directory: {e}")
295
+ return
296
+
297
+ # Get all processed file hashes from database
298
+ processed_hashes = set()
299
+ try:
300
+ results = self.query(
301
+ "SELECT DISTINCT file_hash FROM patients WHERE file_hash IS NOT NULL"
302
+ )
303
+ for r in results:
304
+ if r.get("file_hash"):
305
+ processed_hashes.add(r["file_hash"])
306
+ except Exception as e:
307
+ logger.debug(f"Could not query processed hashes: {e}")
308
+
309
+ # Always show file listing at startup
310
+ if existing_files:
311
+ new_count, _processed_count = self._print_file_listing(
312
+ existing_files, processed_hashes
313
+ )
314
+ else:
315
+ self.console.print_info(f"No intake files found in {self._watch_dir}")
316
+ return
317
+
318
+ # Process new files
319
+ if new_count > 0:
320
+ self.console.print_info(f"Processing {new_count} new file(s)...")
321
+ for f in sorted(existing_files):
322
+ file_hash = compute_file_hash(f)
323
+ if file_hash and file_hash not in processed_hashes:
324
+ self._on_file_created(f)
325
+
326
+ def _get_vlm(self):
327
+ """Get or create VLM client (lazy initialization)."""
328
+ if self._vlm is None:
329
+ try:
330
+ from gaia.llm.vlm_client import VLMClient
331
+
332
+ self.console.print_model_loading(self._vlm_model)
333
+ self._vlm = VLMClient(vlm_model=self._vlm_model)
334
+ self.console.print_model_ready(self._vlm_model)
335
+ logger.debug(f"VLM client initialized: {self._vlm_model}")
336
+ except Exception as e:
337
+ logger.error(f"Failed to initialize VLM: {e}")
338
+ return None
339
+ return self._vlm
340
+
341
+ def _on_file_created(self, path: str) -> None:
342
+ """Handle new file creation in watched directory."""
343
+ file_path = Path(path)
344
+
345
+ # Wait for file to be fully written (Windows file locking)
346
+ time.sleep(0.5)
347
+
348
+ try:
349
+ size = file_path.stat().st_size
350
+ except (FileNotFoundError, OSError):
351
+ size = 0
352
+
353
+ self.console.print_file_created(
354
+ filename=file_path.name,
355
+ size=size,
356
+ extension=file_path.suffix,
357
+ )
358
+
359
+ # Process the file with retry for file locking issues
360
+ max_retries = 3
361
+ for attempt in range(max_retries):
362
+ try:
363
+ self._process_intake_form(path)
364
+ break
365
+ except PermissionError as e:
366
+ if attempt < max_retries - 1:
367
+ logger.warning(
368
+ f"File locked, retrying in 2s ({attempt + 1}/{max_retries}): {e}"
369
+ )
370
+ time.sleep(2.0)
371
+ else:
372
+ logger.error(
373
+ f"Failed to process file after {max_retries} attempts: {e}"
374
+ )
375
+ self.console.print_error(f"Could not access file: {file_path.name}")
376
+
377
+ def _on_file_modified(self, path: str) -> None:
378
+ """Handle file modification (re-process if needed)."""
379
+ # Don't auto-reprocess modified files to avoid duplicates
380
+ _ = path # Intentionally unused - modifications don't trigger reprocessing
381
+
382
+ def _emit_progress(
383
+ self,
384
+ filename: str,
385
+ step_num: int,
386
+ total_steps: int,
387
+ step_name: str,
388
+ status: str = "running",
389
+ ) -> None:
390
+ """
391
+ Emit progress update to console and optional callback.
392
+
393
+ Args:
394
+ filename: Name of file being processed
395
+ step_num: Current step number (1-based)
396
+ total_steps: Total number of processing steps
397
+ step_name: Human-readable step name
398
+ status: 'running', 'complete', or 'error'
399
+ """
400
+ # Update console
401
+ self.console.print_processing_step(step_num, total_steps, step_name, status)
402
+
403
+ # Call external callback if registered (e.g., for SSE events)
404
+ if self._progress_callback:
405
+ try:
406
+ self._progress_callback(
407
+ filename, step_num, total_steps, step_name, status
408
+ )
409
+ except Exception as e:
410
+ logger.debug(f"Progress callback error: {e}")
411
+
412
+ def _process_intake_form(self, file_path: str) -> Optional[Dict[str, Any]]:
413
+ """
414
+ Process an intake form and extract patient data.
415
+
416
+ Args:
417
+ file_path: Path to the intake form (image or PDF)
418
+
419
+ Returns:
420
+ Extracted patient data dict, or None if extraction failed
421
+ """
422
+ path = Path(file_path)
423
+ start_time = time.time()
424
+ self._stats["files_processed"] += 1
425
+ filename = path.name
426
+ total_steps = 7 # Total processing steps
427
+
428
+ logger.debug(f"Processing intake form: {filename}")
429
+
430
+ # Start pipeline progress display
431
+ self.console.print_processing_pipeline_start(filename, total_steps)
432
+
433
+ try:
434
+ # Step 1: Read file
435
+ self._emit_progress(filename, 1, total_steps, "Reading file")
436
+ try:
437
+ with open(path, "rb") as f:
438
+ file_content = f.read()
439
+ except (OSError, IOError) as e:
440
+ logger.error(f"Could not read file: {e}")
441
+ self._emit_progress(filename, 1, total_steps, "Reading file", "error")
442
+ self._stats["extraction_failed"] += 1
443
+ return None
444
+
445
+ # Step 2: Check for duplicates
446
+ self._emit_progress(filename, 2, total_steps, "Checking for duplicates")
447
+ file_hash = compute_file_hash(path)
448
+ if file_hash:
449
+ existing = self.query(
450
+ "SELECT id, first_name, last_name FROM patients WHERE file_hash = ?",
451
+ (file_hash,),
452
+ )
453
+ if existing:
454
+ patient = existing[0]
455
+ name = f"{patient.get('first_name', '')} {patient.get('last_name', '')}".strip()
456
+ self.console.print_info(
457
+ f"Skipping duplicate file (hash: {file_hash[:8]}...) - "
458
+ f"Already processed as patient: {name} (ID: {patient['id']})"
459
+ )
460
+ # Emit duplicate event for Live Feed
461
+ self._emit_progress(
462
+ filename,
463
+ 2,
464
+ total_steps,
465
+ f"Duplicate - already processed as {name}",
466
+ "duplicate",
467
+ )
468
+ # Show completion in console
469
+ self.console.print_processing_pipeline_complete(
470
+ filename,
471
+ True,
472
+ time.time() - start_time,
473
+ name,
474
+ is_duplicate=True,
475
+ )
476
+ return None
477
+
478
+ # Step 3: Prepare and optimize image
479
+ self._emit_progress(filename, 3, total_steps, "Optimizing image")
480
+ image_bytes = self._read_file_as_image(path)
481
+ if image_bytes is None:
482
+ self._emit_progress(
483
+ filename, 3, total_steps, "Optimizing image", "error"
484
+ )
485
+ self._stats["extraction_failed"] += 1
486
+ return None
487
+
488
+ # Step 4: Load VLM model
489
+ self._emit_progress(filename, 4, total_steps, "Loading AI model")
490
+ vlm = self._get_vlm()
491
+ if vlm is None:
492
+ logger.error("VLM not available")
493
+ self._emit_progress(
494
+ filename, 4, total_steps, "Loading AI model", "error"
495
+ )
496
+ self._stats["extraction_failed"] += 1
497
+ return None
498
+
499
+ # Step 5: Extract data with VLM
500
+ self._emit_progress(filename, 5, total_steps, "Extracting patient data")
501
+ mime_type = detect_image_mime_type(image_bytes)
502
+ size_kb = len(image_bytes) / 1024
503
+ self.console.print_extraction_start(1, 1, mime_type)
504
+
505
+ extraction_start = time.time()
506
+ raw_text = vlm.extract_from_image(
507
+ image_bytes=image_bytes,
508
+ prompt=EXTRACTION_PROMPT,
509
+ )
510
+ extraction_time = time.time() - extraction_start
511
+
512
+ # Check for VLM extraction errors (surfaced to user)
513
+ if raw_text.startswith("[VLM extraction failed:"):
514
+ # Extract the error message from the marker
515
+ error_msg = raw_text[1:-1] if raw_text.endswith("]") else raw_text
516
+ self.console.print_error(f"❌ {error_msg}")
517
+ logger.error(f"VLM extraction failed for {path.name}: {error_msg}")
518
+ self._emit_progress(
519
+ filename, 5, total_steps, "Extracting patient data", "error"
520
+ )
521
+ self._stats["extraction_failed"] += 1
522
+ return None
523
+
524
+ self.console.print_extraction_complete(
525
+ len(raw_text), 1, extraction_time, size_kb
526
+ )
527
+
528
+ # Step 6: Parse extraction
529
+ self._emit_progress(filename, 6, total_steps, "Parsing extracted data")
530
+ patient_data = self._parse_extraction(raw_text)
531
+ if patient_data is None:
532
+ logger.warning(f"Failed to parse extraction for: {path.name}")
533
+ self._emit_progress(
534
+ filename, 6, total_steps, "Parsing extracted data", "error"
535
+ )
536
+ self._stats["extraction_failed"] += 1
537
+ return None
538
+
539
+ # Add metadata including file content and hash
540
+ patient_data["source_file"] = str(path.absolute())
541
+ patient_data["raw_extraction"] = raw_text
542
+ patient_data["file_hash"] = file_hash
543
+ patient_data["file_content"] = file_content
544
+
545
+ # Check for returning patient (by name/DOB, not file hash)
546
+ existing_patient = self._find_existing_patient(patient_data)
547
+ is_new_patient = existing_patient is None
548
+ changes_detected = []
549
+
550
+ if existing_patient:
551
+ # Detect changes for returning patient
552
+ changes_detected = self._detect_changes(existing_patient, patient_data)
553
+ patient_data["is_new_patient"] = False
554
+ self._stats["returning_patients"] += 1
555
+ else:
556
+ patient_data["is_new_patient"] = True
557
+ self._stats["new_patients"] += 1
558
+
559
+ # Calculate processing time
560
+ processing_time = time.time() - start_time
561
+ patient_data["processing_time_seconds"] = processing_time
562
+ self._stats["total_processing_time_seconds"] += processing_time
563
+
564
+ # Calculate estimated manual entry time based on extracted data
565
+ estimated_manual = estimate_manual_entry_time(patient_data)
566
+ patient_data["estimated_manual_seconds"] = estimated_manual
567
+ self._stats["total_estimated_manual_seconds"] += estimated_manual
568
+
569
+ # Step 7: Save to database
570
+ self._emit_progress(filename, 7, total_steps, "Saving to database")
571
+ if existing_patient:
572
+ patient_id = self._update_patient(existing_patient["id"], patient_data)
573
+ else:
574
+ patient_id = self._store_patient(patient_data)
575
+
576
+ if patient_id:
577
+ self._stats["extraction_success"] += 1
578
+ patient_data["id"] = patient_id
579
+ patient_data["changes_detected"] = changes_detected
580
+
581
+ # Record intake session for audit trail
582
+ self._record_intake_session(
583
+ patient_id, path, processing_time, is_new_patient, changes_detected
584
+ )
585
+
586
+ # Create alerts for critical items
587
+ self._create_alerts(patient_id, patient_data)
588
+
589
+ self._processed_files.append(
590
+ {
591
+ "file": path.name,
592
+ "patient_id": patient_id,
593
+ "name": f"{patient_data.get('first_name', '')} {patient_data.get('last_name', '')}",
594
+ "is_new_patient": is_new_patient,
595
+ "changes_detected": changes_detected,
596
+ "processing_time_seconds": processing_time,
597
+ "processed_at": time.strftime("%Y-%m-%d %H:%M:%S"),
598
+ }
599
+ )
600
+
601
+ # Limit memory usage - keep only last 1000 entries
602
+ if len(self._processed_files) > 1000:
603
+ self._processed_files = self._processed_files[-1000:]
604
+
605
+ # Show pipeline completion
606
+ patient_name = f"{patient_data.get('first_name', '')} {patient_data.get('last_name', '')}".strip()
607
+ self.console.print_processing_pipeline_complete(
608
+ filename, True, processing_time, patient_name
609
+ )
610
+
611
+ status = "NEW" if is_new_patient else "RETURNING"
612
+ self.console.print_success(
613
+ f"[{status}] Patient record: {patient_data.get('first_name')} "
614
+ f"{patient_data.get('last_name')} (ID: {patient_id})"
615
+ )
616
+
617
+ # Display extracted patient details
618
+ self._print_patient_details(
619
+ patient_data, changes_detected, is_new_patient
620
+ )
621
+
622
+ return patient_data
623
+
624
+ except Exception as e:
625
+ logger.error(f"Error processing {path.name}: {e}")
626
+ self.console.print_processing_pipeline_complete(
627
+ filename, False, time.time() - start_time
628
+ )
629
+ self._stats["extraction_failed"] += 1
630
+
631
+ return None
632
+
633
+ def _print_patient_details(
634
+ self, data: Dict[str, Any], changes: List[Dict[str, Any]], is_new: bool = True
635
+ ) -> None:
636
+ """Print extracted patient details to console using Rich formatting."""
637
+ from rich.console import Console
638
+ from rich.panel import Panel
639
+ from rich.table import Table
640
+
641
+ console = Console()
642
+
643
+ # Fields to skip in display (especially binary/large data)
644
+ skip_fields = {
645
+ "id",
646
+ "source_file",
647
+ "raw_extraction",
648
+ "additional_fields",
649
+ "is_new_patient",
650
+ "processing_time_seconds",
651
+ "changes_detected",
652
+ "created_at",
653
+ "updated_at",
654
+ "file_content", # Binary image data
655
+ "file_hash", # Hash string
656
+ }
657
+
658
+ # Group fields by category with icons
659
+ categories = {
660
+ "👤 Identity": [
661
+ "first_name",
662
+ "last_name",
663
+ "date_of_birth",
664
+ "gender",
665
+ "ssn",
666
+ ],
667
+ "📞 Contact": [
668
+ "phone",
669
+ "mobile_phone",
670
+ "email",
671
+ "address",
672
+ "city",
673
+ "state",
674
+ "zip_code",
675
+ ],
676
+ "🏥 Insurance": ["insurance_provider", "insurance_id", "insurance_group"],
677
+ "💊 Medical": [
678
+ "reason_for_visit",
679
+ "allergies",
680
+ "medications",
681
+ "date_of_injury",
682
+ ],
683
+ "🆘 Emergency": ["emergency_contact_name", "emergency_contact_phone"],
684
+ "💼 Employment": ["employer", "occupation", "work_related_injury"],
685
+ "👨‍⚕️ Provider": ["referring_physician"],
686
+ }
687
+
688
+ # Track changed fields for highlighting
689
+ changed_fields = {c["field"] for c in changes} if changes else set()
690
+
691
+ # Create table for patient details
692
+ table = Table(show_header=False, box=None, padding=(0, 2))
693
+ table.add_column("Field", style="dim")
694
+ table.add_column("Value")
695
+
696
+ displayed_fields = set()
697
+ field_count = 0
698
+
699
+ for category, fields in categories.items():
700
+ category_rows = []
701
+ for field in fields:
702
+ value = data.get(field)
703
+ if value is not None and value != "" and value != "null":
704
+ displayed_fields.add(field)
705
+ field_count += 1
706
+ # Handle boolean values
707
+ if isinstance(value, bool):
708
+ value = "Yes" if value else "No"
709
+ # Style changed fields
710
+ if field in changed_fields:
711
+ category_rows.append(
712
+ (field, f"[bold yellow]{value}[/bold yellow] *")
713
+ )
714
+ else:
715
+ category_rows.append((field, str(value)))
716
+
717
+ if category_rows:
718
+ # Add category header
719
+ table.add_row(f"[bold cyan]{category}[/bold cyan]", "")
720
+ for field, value in category_rows:
721
+ table.add_row(f" {field}", value)
722
+
723
+ # Show additional fields not in categories
724
+ all_category_fields = set()
725
+ for fields in categories.values():
726
+ all_category_fields.update(fields)
727
+
728
+ extra_rows = []
729
+ for key, value in data.items():
730
+ if key not in all_category_fields and key not in skip_fields:
731
+ if value is not None and value != "" and value != "null":
732
+ displayed_fields.add(key)
733
+ field_count += 1
734
+ if isinstance(value, bool):
735
+ value = "Yes" if value else "No"
736
+ if key in changed_fields:
737
+ extra_rows.append(
738
+ (key, f"[bold yellow]{value}[/bold yellow] *")
739
+ )
740
+ else:
741
+ extra_rows.append((key, str(value)))
742
+
743
+ if extra_rows:
744
+ table.add_row("[bold cyan]📋 Additional[/bold cyan]", "")
745
+ for field, value in extra_rows:
746
+ table.add_row(f" {field}", value)
747
+
748
+ # Print patient details in a panel
749
+ console.print(Panel(table, title="Extracted Fields", border_style="blue"))
750
+
751
+ # Summary for returning patients
752
+ if not is_new:
753
+ if changed_fields:
754
+ console.print(
755
+ f"[yellow]⚠️ {len(changed_fields)} field(s) changed:[/yellow] "
756
+ f"{', '.join(changed_fields)}"
757
+ )
758
+ else:
759
+ console.print("[green]✓ All fields identical to previous visit[/green]")
760
+ else:
761
+ console.print(f"[dim]{field_count} fields extracted[/dim]")
762
+
763
+ # Show ready for input prompt
764
+ self.console.print_ready_for_input()
765
+
766
+ def _find_existing_patient(self, data: Dict[str, Any]) -> Optional[Dict[str, Any]]:
767
+ """Check if patient already exists in database."""
768
+ if not data.get("first_name") or not data.get("last_name"):
769
+ return None
770
+
771
+ # Match on name + DOB (most reliable)
772
+ if data.get("date_of_birth"):
773
+ results = self.query(
774
+ """SELECT * FROM patients
775
+ WHERE first_name = :fn AND last_name = :ln AND date_of_birth = :dob
776
+ ORDER BY created_at DESC LIMIT 1""",
777
+ {
778
+ "fn": data["first_name"],
779
+ "ln": data["last_name"],
780
+ "dob": data["date_of_birth"],
781
+ },
782
+ )
783
+ if results:
784
+ return results[0]
785
+
786
+ # Fallback: match on name only (less reliable)
787
+ results = self.query(
788
+ """SELECT * FROM patients
789
+ WHERE first_name = :fn AND last_name = :ln
790
+ ORDER BY created_at DESC LIMIT 1""",
791
+ {"fn": data["first_name"], "ln": data["last_name"]},
792
+ )
793
+ return results[0] if results else None
794
+
795
+ def _detect_changes(
796
+ self, existing: Dict[str, Any], new_data: Dict[str, Any]
797
+ ) -> List[Dict[str, Any]]:
798
+ """Detect changes between existing patient and new data."""
799
+ fields_to_compare = [
800
+ "phone",
801
+ "email",
802
+ "address",
803
+ "city",
804
+ "state",
805
+ "zip_code",
806
+ "insurance_provider",
807
+ "insurance_id",
808
+ "medications",
809
+ "allergies",
810
+ ]
811
+ return detect_field_changes(existing, new_data, fields_to_compare)
812
+
813
+ def _update_patient(self, patient_id: int, data: Dict[str, Any]) -> Optional[int]:
814
+ """Update existing patient record with flexible schema support."""
815
+ try:
816
+ # Separate standard fields from additional fields
817
+ update_data = {}
818
+ additional_fields = {}
819
+
820
+ for key, value in data.items():
821
+ if key in UPDATABLE_COLUMNS:
822
+ update_data[key] = value
823
+ elif key not in ["first_name", "last_name", "date_of_birth", "gender"]:
824
+ # Don't override identity fields, but capture extras
825
+ if value is not None and value != "":
826
+ additional_fields[key] = value
827
+
828
+ # Merge with existing additional_fields if any
829
+ if additional_fields:
830
+ # Get existing additional_fields
831
+ existing = self.query(
832
+ "SELECT additional_fields FROM patients WHERE id = :id",
833
+ {"id": patient_id},
834
+ )
835
+ if existing and existing[0].get("additional_fields"):
836
+ try:
837
+ existing_extra = json.loads(existing[0]["additional_fields"])
838
+ existing_extra.update(additional_fields)
839
+ additional_fields = existing_extra
840
+ except json.JSONDecodeError:
841
+ pass
842
+
843
+ update_data["additional_fields"] = json.dumps(additional_fields)
844
+ logger.info(
845
+ f"Updating {len(additional_fields)} additional fields: "
846
+ f"{list(additional_fields.keys())}"
847
+ )
848
+
849
+ update_data["updated_at"] = time.strftime("%Y-%m-%d %H:%M:%S")
850
+
851
+ # Use mixin's update() method with proper signature
852
+ self.update(
853
+ "patients",
854
+ update_data,
855
+ "id = :id",
856
+ {"id": patient_id},
857
+ )
858
+ logger.info(f"Updated patient record ID: {patient_id}")
859
+ return patient_id
860
+
861
+ except Exception as e:
862
+ logger.error(f"Failed to update patient: {e}")
863
+ return None
864
+
865
+ def _record_intake_session(
866
+ self,
867
+ patient_id: int,
868
+ path: Path,
869
+ processing_time: float,
870
+ is_new_patient: bool,
871
+ changes_detected: List[Dict[str, Any]],
872
+ ) -> None:
873
+ """Record intake session for audit trail."""
874
+ try:
875
+ self.insert(
876
+ "intake_sessions",
877
+ {
878
+ "patient_id": patient_id,
879
+ "source_file": str(path.absolute()),
880
+ "processing_time_seconds": processing_time,
881
+ "is_new_patient": is_new_patient,
882
+ "changes_detected": (
883
+ json.dumps(changes_detected) if changes_detected else None
884
+ ),
885
+ },
886
+ )
887
+ except Exception as e:
888
+ logger.warning(f"Failed to record intake session: {e}")
889
+
890
+ def _create_alerts(self, patient_id: int, data: Dict[str, Any]) -> None:
891
+ """Create alerts for critical items (allergies, missing fields)."""
892
+ try:
893
+ # Critical allergy alert (avoid duplicates for returning patients)
894
+ if data.get("allergies"):
895
+ # Check for existing unacknowledged allergy alert
896
+ existing = self.query(
897
+ """SELECT id FROM alerts
898
+ WHERE patient_id = :pid AND alert_type = 'allergy'
899
+ AND acknowledged = FALSE""",
900
+ {"pid": patient_id},
901
+ )
902
+ if not existing:
903
+ self.insert(
904
+ "alerts",
905
+ {
906
+ "patient_id": patient_id,
907
+ "alert_type": "allergy",
908
+ "priority": "critical",
909
+ "message": f"Patient has allergies: {data['allergies']}",
910
+ "data": json.dumps({"allergies": data["allergies"]}),
911
+ },
912
+ )
913
+
914
+ # Check for missing critical fields
915
+ critical_fields = ["phone", "date_of_birth"]
916
+ missing = [f for f in critical_fields if not data.get(f)]
917
+ if missing:
918
+ # Check for existing unacknowledged missing_field alert
919
+ existing = self.query(
920
+ """SELECT id FROM alerts
921
+ WHERE patient_id = :pid AND alert_type = 'missing_field'
922
+ AND acknowledged = FALSE""",
923
+ {"pid": patient_id},
924
+ )
925
+ if not existing:
926
+ self.insert(
927
+ "alerts",
928
+ {
929
+ "patient_id": patient_id,
930
+ "alert_type": "missing_field",
931
+ "priority": "medium",
932
+ "message": f"Missing critical fields: {', '.join(missing)}",
933
+ "data": json.dumps({"missing_fields": missing}),
934
+ },
935
+ )
936
+
937
+ except Exception as e:
938
+ logger.warning(f"Failed to create alerts: {e}")
939
+
940
+ def _read_file_as_image(self, path: Path) -> Optional[bytes]:
941
+ """Read file and convert to optimized image bytes for VLM processing.
942
+
943
+ Images are automatically resized if they exceed MAX_DIMENSION to improve
944
+ processing speed while maintaining sufficient quality for OCR/extraction.
945
+ """
946
+ suffix = path.suffix.lower()
947
+
948
+ if suffix == ".pdf":
949
+ # Convert PDF first page to image (already optimized in pdf_page_to_image)
950
+ return self._pdf_to_image(path)
951
+ elif suffix in [".png", ".jpg", ".jpeg", ".tiff", ".bmp"]:
952
+ raw_bytes = path.read_bytes()
953
+ return self._optimize_image(raw_bytes)
954
+ else:
955
+ logger.warning(f"Unsupported file type: {suffix}")
956
+ return None
957
+
958
+ def _optimize_image(
959
+ self,
960
+ image_bytes: bytes,
961
+ max_dimension: int = 1024,
962
+ jpeg_quality: int = 85,
963
+ ) -> bytes:
964
+ """
965
+ Optimize image for VLM processing by resizing large images.
966
+
967
+ Reduces image dimensions while maintaining quality sufficient for OCR
968
+ and text extraction. This dramatically improves processing speed for
969
+ high-resolution scans and photos.
970
+
971
+ Images are padded to square dimensions to avoid a Vulkan backend bug
972
+ in llama.cpp where the UPSCALE operator is unsupported for certain
973
+ non-square aspect ratios (particularly landscape orientations).
974
+
975
+ Args:
976
+ image_bytes: Raw image bytes (PNG, JPEG, etc.)
977
+ max_dimension: Maximum width or height (default: 1024px)
978
+ jpeg_quality: JPEG compression quality 1-100 (default: 85)
979
+
980
+ Returns:
981
+ Optimized image bytes (JPEG format, square dimensions)
982
+ """
983
+ import io
984
+
985
+ try:
986
+ from PIL import Image, ImageOps
987
+
988
+ # Load image from bytes
989
+ img = Image.open(io.BytesIO(image_bytes))
990
+
991
+ # Apply EXIF orientation - phone photos are often stored landscape
992
+ # but have EXIF metadata indicating they should be displayed as portrait
993
+ img = ImageOps.exif_transpose(img)
994
+
995
+ original_width, original_height = img.size
996
+ original_size_kb = len(image_bytes) / 1024
997
+
998
+ # Convert to RGB early if needed (for JPEG output)
999
+ if img.mode in ("RGBA", "P"):
1000
+ img = img.convert("RGB")
1001
+
1002
+ # Check if resizing is needed
1003
+ if original_width <= max_dimension and original_height <= max_dimension:
1004
+ new_width, new_height = original_width, original_height
1005
+ else:
1006
+ # Calculate new dimensions maintaining aspect ratio
1007
+ scale = min(
1008
+ max_dimension / original_width, max_dimension / original_height
1009
+ )
1010
+ new_width = int(original_width * scale)
1011
+ new_height = int(original_height * scale)
1012
+
1013
+ # Resize with high-quality LANCZOS filter
1014
+ img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
1015
+
1016
+ # Pad to square to avoid Vulkan UPSCALE bug with non-square images
1017
+ # The bug causes timeouts with landscape orientations (e.g., 1024x768)
1018
+ if new_width != new_height:
1019
+ square_size = max(new_width, new_height)
1020
+ # Create white square canvas
1021
+ square_img = Image.new(
1022
+ "RGB", (square_size, square_size), (255, 255, 255)
1023
+ )
1024
+ # Center the image on the canvas
1025
+ x_offset = (square_size - new_width) // 2
1026
+ y_offset = (square_size - new_height) // 2
1027
+ square_img.paste(img, (x_offset, y_offset))
1028
+ img = square_img
1029
+ final_size = square_size
1030
+ was_padded = True
1031
+ else:
1032
+ final_size = new_width
1033
+ was_padded = False
1034
+
1035
+ # Save as optimized JPEG
1036
+ output = io.BytesIO()
1037
+ img.save(output, format="JPEG", quality=jpeg_quality, optimize=True)
1038
+ optimized_bytes = output.getvalue()
1039
+
1040
+ optimized_size_kb = len(optimized_bytes) / 1024
1041
+ reduction_pct = (1 - optimized_size_kb / original_size_kb) * 100
1042
+
1043
+ # Show optimization results to user
1044
+ if was_padded:
1045
+ self.console.print_info(
1046
+ f"Image resized: {original_width}x{original_height} → "
1047
+ f"{final_size}x{final_size} (padded to square, "
1048
+ f"{original_size_kb:.0f}KB → {optimized_size_kb:.0f}KB, "
1049
+ f"{reduction_pct:.0f}% smaller)"
1050
+ )
1051
+ else:
1052
+ self.console.print_info(
1053
+ f"Image resized: {original_width}x{original_height} → "
1054
+ f"{new_width}x{new_height} ({original_size_kb:.0f}KB → "
1055
+ f"{optimized_size_kb:.0f}KB, {reduction_pct:.0f}% smaller)"
1056
+ )
1057
+
1058
+ logger.info(
1059
+ f"Image optimized: {original_width}x{original_height} → "
1060
+ f"{final_size}x{final_size}, {original_size_kb:.0f}KB → "
1061
+ f"{optimized_size_kb:.0f}KB ({reduction_pct:.0f}% reduction)"
1062
+ f"{' (padded to square)' if was_padded else ''}"
1063
+ )
1064
+
1065
+ return optimized_bytes
1066
+
1067
+ except ImportError:
1068
+ logger.warning("PIL not available, returning original image")
1069
+ return image_bytes
1070
+ except Exception as e:
1071
+ logger.warning(f"Image optimization failed: {e}, using original")
1072
+ return image_bytes
1073
+
1074
+ def _pdf_to_image(self, pdf_path: Path) -> Optional[bytes]:
1075
+ """Convert first page of PDF to image bytes."""
1076
+ return pdf_page_to_image(pdf_path, page=0, scale=2.0)
1077
+
1078
+ def _parse_extraction(self, raw_text: str) -> Optional[Dict[str, Any]]:
1079
+ """Parse extracted text into structured patient data."""
1080
+ result = extract_json_from_text(raw_text)
1081
+ if result is None:
1082
+ logger.warning("No valid JSON found in extraction")
1083
+ return None
1084
+
1085
+ # Normalize phone fields: prefer mobile_phone if phone is not set
1086
+ # This handles forms where VLM extracts to mobile_phone instead of phone
1087
+ if not result.get("phone"):
1088
+ for phone_field in [
1089
+ "mobile_phone",
1090
+ "home_phone",
1091
+ "work_phone",
1092
+ "cell_phone",
1093
+ ]:
1094
+ if result.get(phone_field):
1095
+ result["phone"] = result[phone_field]
1096
+ logger.debug(
1097
+ f"Normalized {phone_field} to phone: {result['phone']}"
1098
+ )
1099
+ break
1100
+
1101
+ # Also check emergency_contact_phone normalization
1102
+ if not result.get("emergency_contact_phone"):
1103
+ for ec_phone in ["emergency_phone", "emergency_contact"]:
1104
+ if result.get(ec_phone) and isinstance(result[ec_phone], str):
1105
+ # Check if it looks like a phone number
1106
+ if any(c.isdigit() for c in result[ec_phone]):
1107
+ result["emergency_contact_phone"] = result[ec_phone]
1108
+ break
1109
+
1110
+ return result
1111
+
1112
+ def _store_patient(self, data: Dict[str, Any]) -> Optional[int]:
1113
+ """Store patient data in database with flexible schema support."""
1114
+ try:
1115
+ # Validate required fields
1116
+ if not data.get("first_name") or not data.get("last_name"):
1117
+ logger.error("Missing required fields: first_name and/or last_name")
1118
+ self.console.print_error("Cannot store patient: missing name fields")
1119
+ return None
1120
+
1121
+ # Separate standard fields from additional fields
1122
+ insert_data = {}
1123
+ additional_fields = {}
1124
+
1125
+ for key, value in data.items():
1126
+ if key in STANDARD_COLUMNS:
1127
+ insert_data[key] = value
1128
+ elif value is not None and value != "":
1129
+ # Store non-empty extra fields in additional_fields
1130
+ additional_fields[key] = value
1131
+
1132
+ # Store additional fields as JSON if any exist
1133
+ if additional_fields:
1134
+ insert_data["additional_fields"] = json.dumps(additional_fields)
1135
+ logger.info(
1136
+ f"Storing {len(additional_fields)} additional fields: "
1137
+ f"{list(additional_fields.keys())}"
1138
+ )
1139
+
1140
+ patient_id = self.insert("patients", insert_data)
1141
+ logger.info(f"Stored patient record ID: {patient_id}")
1142
+ return patient_id
1143
+
1144
+ except Exception as e:
1145
+ logger.error(f"Failed to store patient: {e}")
1146
+ self.console.print_error(f"Database error: {str(e)}")
1147
+ return None
1148
+
1149
+ def _get_system_prompt(self) -> str:
1150
+ """Generate the system prompt for the intake agent."""
1151
+ return f"""You are a Medical Intake Assistant managing patient records.
1152
+
1153
+ You have access to a database of patient intake forms that were automatically processed.
1154
+
1155
+ **Your Capabilities:**
1156
+ - Search for patients by name, DOB, or other criteria
1157
+ - View patient details and intake information
1158
+ - Report on processing statistics
1159
+ - Answer questions about patient data
1160
+
1161
+ **Current Status:**
1162
+ - Watching directory: {self._watch_dir}
1163
+ - Database: {self._db_path}
1164
+ - Files processed: {self._stats.get('files_processed', 0)}
1165
+ - Successful extractions: {self._stats.get('extraction_success', 0)}
1166
+
1167
+ **Important:**
1168
+ - Always protect patient privacy
1169
+ - Only report data that was extracted from forms
1170
+ - If asked about a patient not in the database, say so clearly
1171
+
1172
+ Use the available tools to search and retrieve patient information."""
1173
+
1174
+ def _register_tools(self):
1175
+ """Register patient management tools."""
1176
+ agent = self
1177
+
1178
+ @tool
1179
+ def search_patients(
1180
+ name: Optional[str] = None,
1181
+ date_of_birth: Optional[str] = None,
1182
+ ) -> Dict[str, Any]:
1183
+ """
1184
+ Search for patients by name or date of birth.
1185
+
1186
+ Args:
1187
+ name: Patient name (first, last, or full name)
1188
+ date_of_birth: Date of birth (YYYY-MM-DD format)
1189
+
1190
+ Returns:
1191
+ Dict with matching patients
1192
+ """
1193
+ conditions = []
1194
+ params = {}
1195
+
1196
+ if name:
1197
+ conditions.append(
1198
+ "(first_name LIKE :name OR last_name LIKE :name "
1199
+ "OR (first_name || ' ' || last_name) LIKE :name)"
1200
+ )
1201
+ params["name"] = f"%{name}%"
1202
+
1203
+ if date_of_birth:
1204
+ conditions.append("date_of_birth = :dob")
1205
+ params["dob"] = date_of_birth
1206
+
1207
+ if not conditions:
1208
+ # Return recent patients if no criteria
1209
+ query = """
1210
+ SELECT id, first_name, last_name, date_of_birth,
1211
+ phone, reason_for_visit, created_at
1212
+ FROM patients
1213
+ ORDER BY created_at DESC
1214
+ LIMIT 10
1215
+ """
1216
+ params = {}
1217
+ else:
1218
+ query = f"""
1219
+ SELECT id, first_name, last_name, date_of_birth,
1220
+ phone, reason_for_visit, created_at
1221
+ FROM patients
1222
+ WHERE {' AND '.join(conditions)}
1223
+ ORDER BY created_at DESC
1224
+ """
1225
+
1226
+ results = agent.query(query, params)
1227
+ return {
1228
+ "patients": results,
1229
+ "count": len(results),
1230
+ "query": {"name": name, "date_of_birth": date_of_birth},
1231
+ }
1232
+
1233
+ @tool
1234
+ def get_patient(patient_id: int) -> Dict[str, Any]:
1235
+ """
1236
+ Get full details for a specific patient.
1237
+
1238
+ Args:
1239
+ patient_id: The patient's database ID
1240
+
1241
+ Returns:
1242
+ Dict with patient details
1243
+ """
1244
+ results = agent.query(
1245
+ "SELECT * FROM patients WHERE id = :id",
1246
+ {"id": patient_id},
1247
+ )
1248
+
1249
+ if results:
1250
+ patient = results[0]
1251
+ # Remove large/binary fields - don't send to LLM
1252
+ patient.pop("raw_extraction", None)
1253
+ patient.pop("file_content", None) # Image bytes
1254
+ patient.pop("embedding", None) # Vector embedding
1255
+ # Truncate file_hash for display
1256
+ if patient.get("file_hash"):
1257
+ patient["file_hash"] = patient["file_hash"][:12] + "..."
1258
+ return {"found": True, "patient": patient}
1259
+ return {"found": False, "message": f"Patient ID {patient_id} not found"}
1260
+
1261
+ @tool
1262
+ def list_recent_patients(limit: int = 10) -> Dict[str, Any]:
1263
+ """
1264
+ List recently processed patients.
1265
+
1266
+ Args:
1267
+ limit: Maximum number of patients to return (default: 10)
1268
+
1269
+ Returns:
1270
+ Dict with recent patients
1271
+ """
1272
+ results = agent.query(
1273
+ """
1274
+ SELECT id, first_name, last_name, date_of_birth,
1275
+ reason_for_visit, created_at
1276
+ FROM patients
1277
+ ORDER BY created_at DESC
1278
+ LIMIT :limit
1279
+ """,
1280
+ {"limit": limit},
1281
+ )
1282
+ return {"patients": results, "count": len(results)}
1283
+
1284
+ @tool
1285
+ def get_intake_stats() -> Dict[str, Any]:
1286
+ """
1287
+ Get statistics about intake form processing.
1288
+
1289
+ Returns:
1290
+ Dict with processing statistics
1291
+ """
1292
+ return agent.get_stats()
1293
+
1294
+ @tool
1295
+ def process_file(file_path: str) -> Dict[str, Any]:
1296
+ """
1297
+ Manually process an intake form file.
1298
+
1299
+ Args:
1300
+ file_path: Path to the intake form file
1301
+
1302
+ Returns:
1303
+ Dict with processing result
1304
+ """
1305
+ path = Path(file_path)
1306
+ if not path.exists():
1307
+ return {"success": False, "error": f"File not found: {file_path}"}
1308
+
1309
+ # pylint: disable=protected-access
1310
+ result = agent._process_intake_form(str(path))
1311
+ if result:
1312
+ return {
1313
+ "success": True,
1314
+ "patient_id": result.get("id"),
1315
+ "name": f"{result.get('first_name', '')} {result.get('last_name', '')}",
1316
+ }
1317
+ return {"success": False, "error": "Failed to extract patient data"}
1318
+
1319
+ def get_stats(self) -> Dict[str, Any]:
1320
+ """
1321
+ Get statistics about intake form processing.
1322
+
1323
+ Returns:
1324
+ Dict with processing statistics including:
1325
+ - total_patients: Total patient count
1326
+ - processed_today: Patients processed today
1327
+ - new_patients: New patient count
1328
+ - returning_patients: Returning patient count
1329
+ - files_processed: Total files processed
1330
+ - extraction_success/failed: Success/failure counts
1331
+ - success_rate: Success percentage
1332
+ - time_saved_minutes/percent: Time savings metrics
1333
+ - avg_processing_seconds: Average processing time
1334
+ - unacknowledged_alerts: Alert count
1335
+ - watching_directory: Watched directory path
1336
+ - uptime_seconds: Agent uptime
1337
+ """
1338
+ # Get total patient count
1339
+ count_result = self.query("SELECT COUNT(*) as count FROM patients")
1340
+ total_patients = count_result[0]["count"] if count_result else 0
1341
+
1342
+ # Get today's count
1343
+ today_result = self.query(
1344
+ "SELECT COUNT(*) as count FROM patients WHERE date(created_at) = date('now')"
1345
+ )
1346
+ today_count = today_result[0]["count"] if today_result else 0
1347
+
1348
+ # Get unacknowledged alerts count
1349
+ alerts_result = self.query(
1350
+ "SELECT COUNT(*) as count FROM alerts WHERE acknowledged = FALSE"
1351
+ )
1352
+ unacknowledged_alerts = alerts_result[0]["count"] if alerts_result else 0
1353
+
1354
+ # Calculate time savings based on actual extracted data
1355
+ # Uses estimated manual entry time calculated per-form based on fields/characters
1356
+ estimated_manual_seconds = self._stats["total_estimated_manual_seconds"]
1357
+ actual_processing_seconds = self._stats["total_processing_time_seconds"]
1358
+ time_saved_seconds = max(
1359
+ 0, estimated_manual_seconds - actual_processing_seconds
1360
+ )
1361
+
1362
+ # Calculate percentage improvement
1363
+ if estimated_manual_seconds > 0:
1364
+ time_saved_percent = (time_saved_seconds / estimated_manual_seconds) * 100
1365
+ else:
1366
+ time_saved_percent = 0
1367
+
1368
+ # Average estimated manual time per form
1369
+ successful = self._stats["extraction_success"]
1370
+ avg_manual_seconds = (
1371
+ estimated_manual_seconds / successful if successful > 0 else 0
1372
+ )
1373
+
1374
+ return {
1375
+ "total_patients": total_patients,
1376
+ "processed_today": today_count,
1377
+ "new_patients": self._stats["new_patients"],
1378
+ "returning_patients": self._stats["returning_patients"],
1379
+ "files_processed": self._stats["files_processed"],
1380
+ "extraction_success": self._stats["extraction_success"],
1381
+ "extraction_failed": self._stats["extraction_failed"],
1382
+ "success_rate": (
1383
+ f"{(self._stats['extraction_success'] / self._stats['files_processed'] * 100):.1f}%"
1384
+ if self._stats["files_processed"] > 0
1385
+ else "N/A"
1386
+ ),
1387
+ # Total cumulative metrics
1388
+ "time_saved_seconds": round(time_saved_seconds, 1),
1389
+ "time_saved_minutes": round(time_saved_seconds / 60, 1),
1390
+ "time_saved_percent": f"{time_saved_percent:.0f}%",
1391
+ "total_estimated_manual_seconds": round(estimated_manual_seconds, 1),
1392
+ "total_ai_processing_seconds": round(actual_processing_seconds, 1),
1393
+ # Per-form averages
1394
+ "avg_manual_seconds": round(avg_manual_seconds, 1),
1395
+ "avg_processing_seconds": (
1396
+ round(actual_processing_seconds / successful, 1)
1397
+ if successful > 0
1398
+ else 0
1399
+ ),
1400
+ # Legacy field name for backwards compatibility
1401
+ "estimated_manual_seconds": round(estimated_manual_seconds, 1),
1402
+ "unacknowledged_alerts": unacknowledged_alerts,
1403
+ "watching_directory": str(self._watch_dir),
1404
+ "uptime_seconds": int(time.time() - self._stats["start_time"]),
1405
+ }
1406
+
1407
+ def clear_database(self) -> Dict[str, Any]:
1408
+ """
1409
+ Clear all data from the database and reset statistics.
1410
+
1411
+ This removes all patients, alerts, and intake sessions, providing
1412
+ a clean slate for fresh processing.
1413
+
1414
+ Returns:
1415
+ Dict with counts of deleted records
1416
+ """
1417
+ logger.info("Clearing database...")
1418
+ counts = {}
1419
+
1420
+ try:
1421
+ # Get counts before deletion
1422
+ for table in ["patients", "alerts", "intake_sessions"]:
1423
+ result = self.query(f"SELECT COUNT(*) as count FROM {table}")
1424
+ counts[table] = result[0]["count"] if result else 0
1425
+
1426
+ # Delete all records from each table
1427
+ self.execute("DELETE FROM intake_sessions")
1428
+ self.execute("DELETE FROM alerts")
1429
+ self.execute("DELETE FROM patients")
1430
+
1431
+ # Reset in-memory statistics
1432
+ self._stats = {
1433
+ "files_processed": 0,
1434
+ "extraction_success": 0,
1435
+ "extraction_failed": 0,
1436
+ "new_patients": 0,
1437
+ "returning_patients": 0,
1438
+ "total_processing_time_seconds": 0.0,
1439
+ "total_estimated_manual_seconds": 0.0,
1440
+ "start_time": time.time(),
1441
+ }
1442
+
1443
+ # Clear processed files list
1444
+ self._processed_files = []
1445
+
1446
+ logger.info(
1447
+ f"Database cleared: {counts.get('patients', 0)} patients, "
1448
+ f"{counts.get('alerts', 0)} alerts, "
1449
+ f"{counts.get('intake_sessions', 0)} sessions"
1450
+ )
1451
+
1452
+ return {
1453
+ "success": True,
1454
+ "deleted": counts,
1455
+ "message": "Database cleared successfully",
1456
+ }
1457
+
1458
+ except Exception as e:
1459
+ logger.error(f"Failed to clear database: {e}")
1460
+ return {
1461
+ "success": False,
1462
+ "error": str(e),
1463
+ "message": "Failed to clear database",
1464
+ }
1465
+
1466
+ def stop(self) -> None:
1467
+ """Stop the agent and clean up resources."""
1468
+ logger.info("Stopping Medical Intake Agent...")
1469
+ errors = []
1470
+
1471
+ # Stop file watchers
1472
+ try:
1473
+ self.stop_all_watchers()
1474
+ except Exception as e:
1475
+ errors.append(f"Failed to stop watchers: {e}")
1476
+ logger.error(errors[-1])
1477
+
1478
+ # Close database
1479
+ try:
1480
+ self.close_db()
1481
+ except Exception as e:
1482
+ errors.append(f"Failed to close database: {e}")
1483
+ logger.error(errors[-1])
1484
+
1485
+ # Cleanup VLM
1486
+ try:
1487
+ if self._vlm:
1488
+ self._vlm.cleanup()
1489
+ self._vlm = None
1490
+ except Exception as e:
1491
+ errors.append(f"Failed to cleanup VLM: {e}")
1492
+ logger.error(errors[-1])
1493
+
1494
+ if errors:
1495
+ logger.warning(f"Cleanup completed with {len(errors)} error(s)")
1496
+ else:
1497
+ logger.info("Medical Intake Agent stopped")
1498
+
1499
+ def __enter__(self):
1500
+ """Context manager entry."""
1501
+ return self
1502
+
1503
+ def __exit__(self, exc_type, exc_val, exc_tb):
1504
+ """Context manager exit with cleanup."""
1505
+ self.stop()
1506
+ return False