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,1974 @@
1
+ # Copyright(C) 2025-2026 Advanced Micro Devices, Inc. All rights reserved.
2
+ # SPDX-License-Identifier: MIT
3
+
4
+ """FastAPI server for EMR Dashboard with SSE support."""
5
+
6
+ # pylint: disable=protected-access
7
+ # Dashboard server intentionally accesses agent internals to hook into
8
+ # processing events, patch methods for SSE notifications, and read config.
9
+
10
+ import asyncio
11
+ import json
12
+ import logging
13
+ import threading
14
+ import time
15
+ from datetime import datetime
16
+ from pathlib import Path
17
+ from typing import Any, Dict, List, Optional, Set
18
+
19
+ from pydantic import BaseModel
20
+
21
+ try:
22
+ import uvicorn
23
+ from fastapi import FastAPI, File, HTTPException, UploadFile
24
+ from fastapi.middleware.cors import CORSMiddleware
25
+ from fastapi.responses import FileResponse, Response, StreamingResponse
26
+ from fastapi.staticfiles import StaticFiles
27
+
28
+ FASTAPI_AVAILABLE = True
29
+ except ImportError:
30
+ FASTAPI_AVAILABLE = False
31
+ # Placeholders for when FastAPI is not installed
32
+ uvicorn = None # type: ignore[assignment]
33
+ FastAPI = None # type: ignore[assignment,misc]
34
+ File = None # type: ignore[assignment]
35
+ HTTPException = None # type: ignore[assignment,misc]
36
+ UploadFile = None # type: ignore[assignment]
37
+ CORSMiddleware = None # type: ignore[assignment]
38
+ FileResponse = None # type: ignore[assignment]
39
+ Response = None # type: ignore[assignment]
40
+ StreamingResponse = None # type: ignore[assignment]
41
+ StaticFiles = None # type: ignore[assignment]
42
+
43
+ from gaia.agents.emr.agent import MedicalIntakeAgent
44
+
45
+ logger = logging.getLogger(__name__)
46
+
47
+
48
+ def _safe_json_default(obj: Any) -> Any:
49
+ """Fallback serializer for non-standard JSON types."""
50
+ if isinstance(obj, bytes):
51
+ return f"<binary: {len(obj)} bytes>"
52
+ elif hasattr(obj, "isoformat"):
53
+ return obj.isoformat()
54
+ elif hasattr(obj, "__dict__"):
55
+ return obj.__dict__
56
+ return str(obj)
57
+
58
+
59
+ def _safe_json_dumps(obj: Any) -> str:
60
+ """JSON dumps with fallback for non-serializable types like bytes."""
61
+ return json.dumps(obj, default=_safe_json_default)
62
+
63
+
64
+ # Pydantic models for request validation
65
+ class WatchDirConfig(BaseModel):
66
+ """Request model for watch directory configuration."""
67
+
68
+ watch_dir: str
69
+
70
+
71
+ class ChatRequest(BaseModel):
72
+ """Request model for chat messages."""
73
+
74
+ message: str
75
+
76
+
77
+ class PatientUpdateRequest(BaseModel):
78
+ """Request model for updating patient data."""
79
+
80
+ first_name: Optional[str] = None
81
+ last_name: Optional[str] = None
82
+ date_of_birth: Optional[str] = None
83
+ gender: Optional[str] = None
84
+ phone: Optional[str] = None
85
+ email: Optional[str] = None
86
+ address: Optional[str] = None
87
+ city: Optional[str] = None
88
+ state: Optional[str] = None
89
+ zip_code: Optional[str] = None
90
+ insurance_provider: Optional[str] = None
91
+ insurance_id: Optional[str] = None
92
+ reason_for_visit: Optional[str] = None
93
+ allergies: Optional[str] = None
94
+ medications: Optional[str] = None
95
+ emergency_contact_name: Optional[str] = None
96
+ emergency_contact_phone: Optional[str] = None
97
+
98
+
99
+ # Global state
100
+ _agent_instance: Optional[MedicalIntakeAgent] = None
101
+ _agent_lock = threading.Lock()
102
+
103
+ # Per-client SSE queues for multi-client broadcast
104
+ _sse_clients: List[asyncio.Queue] = []
105
+ _sse_clients_lock = asyncio.Lock()
106
+
107
+ # Store the main event loop reference for thread-safe broadcasting
108
+ _main_event_loop: Optional[asyncio.AbstractEventLoop] = None
109
+
110
+ # Track currently processing file for status display
111
+ _current_processing_file: Optional[str] = None
112
+ _processing_lock = threading.Lock()
113
+
114
+ # Track files being processed via upload API (to prevent file watcher double-processing)
115
+ _api_processing_files: Set[str] = set()
116
+ _api_processing_lock = threading.Lock()
117
+
118
+ # Thread-local storage to mark API-initiated processing (skip the duplicate check)
119
+ _thread_local = threading.local()
120
+
121
+ # Track failed file hashes to show "failed" status in watch folder
122
+ _failed_file_hashes: Set[str] = set()
123
+ _failed_lock = threading.Lock()
124
+
125
+ # Ring buffer of recent events to replay to new SSE clients
126
+ _recent_events: List[Dict[str, Any]] = []
127
+ _recent_events_lock = threading.Lock() # Use threading.Lock for cross-thread access
128
+ _MAX_RECENT_EVENTS = 20 # Keep last 20 events for replay
129
+
130
+
131
+ async def broadcast_event(event: Dict[str, Any]) -> None:
132
+ """Broadcast event to all SSE clients and store for replay."""
133
+ # Store in recent events buffer (only patient_created events for replay)
134
+ if event.get("type") == "patient_created":
135
+ with _recent_events_lock:
136
+ _recent_events.append(event)
137
+ # Keep only the most recent events
138
+ if len(_recent_events) > _MAX_RECENT_EVENTS:
139
+ _recent_events.pop(0)
140
+
141
+ async with _sse_clients_lock:
142
+ for client_queue in _sse_clients:
143
+ try:
144
+ client_queue.put_nowait(event)
145
+ except asyncio.QueueFull:
146
+ logger.warning("Client queue full, dropping event")
147
+
148
+
149
+ def _broadcast_sync(event: Dict[str, Any]) -> None:
150
+ """Thread-safe helper to broadcast event from any context (including file watcher thread)."""
151
+ if _main_event_loop is None:
152
+ logger.warning("Main event loop not set, cannot broadcast event")
153
+ return
154
+
155
+ try:
156
+ # Use the stored main event loop reference for thread-safe broadcasting
157
+ asyncio.run_coroutine_threadsafe(broadcast_event(event), _main_event_loop)
158
+ except Exception as e:
159
+ logger.error(f"Error broadcasting event: {e}")
160
+
161
+
162
+ class DashboardEventHandler:
163
+ """Handler that publishes agent events to SSE clients."""
164
+
165
+ @staticmethod
166
+ def on_patient_created(patient_data: Dict[str, Any]):
167
+ """Publish patient created event."""
168
+ event = {
169
+ "type": "patient_created",
170
+ "data": patient_data,
171
+ "timestamp": datetime.now().isoformat(),
172
+ }
173
+ _broadcast_sync(event)
174
+
175
+ @staticmethod
176
+ def on_processing_started(filename: str):
177
+ """Publish processing started event."""
178
+ event = {
179
+ "type": "processing_started",
180
+ "data": {"filename": filename},
181
+ "timestamp": datetime.now().isoformat(),
182
+ }
183
+ _broadcast_sync(event)
184
+
185
+ @staticmethod
186
+ def on_processing_completed(
187
+ filename: str,
188
+ success: bool,
189
+ patient_id: Optional[int] = None,
190
+ is_duplicate: bool = False,
191
+ patient_name: str = None,
192
+ ):
193
+ """Publish processing completed event."""
194
+ event = {
195
+ "type": "processing_completed",
196
+ "data": {
197
+ "filename": filename,
198
+ "success": success,
199
+ "patient_id": patient_id,
200
+ "is_duplicate": is_duplicate,
201
+ "patient_name": patient_name,
202
+ },
203
+ "timestamp": datetime.now().isoformat(),
204
+ }
205
+ _broadcast_sync(event)
206
+
207
+ @staticmethod
208
+ def on_status_update(filename: str, status: str, detail: str = ""):
209
+ """Publish processing status update."""
210
+ event = {
211
+ "type": "status_update",
212
+ "data": {
213
+ "filename": filename,
214
+ "status": status,
215
+ "detail": detail,
216
+ },
217
+ "timestamp": datetime.now().isoformat(),
218
+ }
219
+ _broadcast_sync(event)
220
+
221
+ @staticmethod
222
+ def on_processing_error(filename: str, error: str, error_type: str = "error"):
223
+ """Publish processing error event."""
224
+ event = {
225
+ "type": "processing_error",
226
+ "data": {
227
+ "filename": filename,
228
+ "error": error,
229
+ "error_type": error_type,
230
+ },
231
+ "timestamp": datetime.now().isoformat(),
232
+ }
233
+ _broadcast_sync(event)
234
+
235
+ @staticmethod
236
+ def on_processing_step(
237
+ filename: str,
238
+ step_num: int,
239
+ total_steps: int,
240
+ step_name: str,
241
+ status: str = "running",
242
+ ):
243
+ """Publish processing step event for progress tracking."""
244
+ event = {
245
+ "type": "processing_step",
246
+ "data": {
247
+ "filename": filename,
248
+ "step_num": step_num,
249
+ "total_steps": total_steps,
250
+ "step_name": step_name,
251
+ "status": status,
252
+ },
253
+ "timestamp": datetime.now().isoformat(),
254
+ }
255
+ _broadcast_sync(event)
256
+
257
+
258
+ def create_app(
259
+ watch_dir: str = "./intake_forms",
260
+ db_path: str = "./data/patients.db",
261
+ ) -> FastAPI:
262
+ """
263
+ Create FastAPI app for EMR dashboard.
264
+
265
+ Args:
266
+ watch_dir: Directory to watch for intake forms
267
+ db_path: Path to patient database
268
+
269
+ Returns:
270
+ FastAPI application instance
271
+ """
272
+ if not FASTAPI_AVAILABLE:
273
+ raise ImportError(
274
+ "FastAPI not installed. Install with: pip install 'amd-gaia[api]'"
275
+ )
276
+
277
+ app = FastAPI(
278
+ title="GAIA Medical Intake Dashboard",
279
+ description="Real-time patient intake monitoring with AMD Ryzen AI",
280
+ version="0.1.0",
281
+ )
282
+
283
+ # Enable CORS for development
284
+ app.add_middleware(
285
+ CORSMiddleware,
286
+ allow_origins=["*"],
287
+ allow_credentials=True,
288
+ allow_methods=["*"],
289
+ allow_headers=["*"],
290
+ )
291
+
292
+ def start_agent():
293
+ """Start agent in background thread."""
294
+ global _agent_instance
295
+
296
+ # Wait for dashboard/Electron to connect before processing files
297
+ # This gives the UI time to establish SSE connection
298
+ logger.info("Waiting 2s for dashboard to connect...")
299
+ time.sleep(2.0)
300
+
301
+ with _agent_lock:
302
+ if _agent_instance is not None:
303
+ logger.warning("Agent already initialized")
304
+ return
305
+
306
+ # Initialize with auto_start_watching=False, then start manually
307
+ # This ensures SSE clients are connected before processing begins
308
+ _agent_instance = MedicalIntakeAgent(
309
+ watch_dir=watch_dir,
310
+ db_path=db_path,
311
+ auto_start_watching=False,
312
+ )
313
+
314
+ # Patch agent to publish events
315
+ original_process = _agent_instance._process_intake_form
316
+ original_get_vlm = _agent_instance._get_vlm
317
+ original_store_patient = _agent_instance._store_patient
318
+
319
+ # Track current file being processed for context
320
+ _current_file = {"name": None}
321
+
322
+ def patched_get_vlm():
323
+ filename = _current_file.get("name", "file")
324
+ if _agent_instance._vlm is None:
325
+ DashboardEventHandler.on_processing_step(
326
+ filename, 4, 7, "Loading AI model", "running"
327
+ )
328
+ result = original_get_vlm()
329
+ if result and _agent_instance._vlm is not None:
330
+ DashboardEventHandler.on_processing_step(
331
+ filename, 5, 7, "Extracting data", "running"
332
+ )
333
+ return result
334
+
335
+ def patched_store_patient(data):
336
+ filename = _current_file.get("name", "file")
337
+ # Emit storing step
338
+ DashboardEventHandler.on_processing_step(
339
+ filename, 7, 7, "Saving to database", "running"
340
+ )
341
+ # Check for missing required fields before calling original
342
+ if not data.get("first_name") or not data.get("last_name"):
343
+ DashboardEventHandler.on_processing_error(
344
+ filename,
345
+ "Missing required fields: first_name and/or last_name",
346
+ "validation_error",
347
+ )
348
+ return original_store_patient(data)
349
+
350
+ # Track duplicate detection per file
351
+ _duplicate_info = {"is_duplicate": False, "patient_name": None}
352
+
353
+ def patched_process(file_path: str):
354
+ global _current_processing_file
355
+ nonlocal _duplicate_info
356
+ filename = Path(file_path).name
357
+ _current_file["name"] = filename
358
+ _duplicate_info = {"is_duplicate": False, "patient_name": None}
359
+
360
+ # Skip if file is being processed via upload API (prevents double-processing)
361
+ # But don't skip if this IS the API call (marked via thread-local flag)
362
+ is_api_call = getattr(_thread_local, "is_api_call", False)
363
+ if not is_api_call:
364
+ with _api_processing_lock:
365
+ if filename in _api_processing_files:
366
+ logger.info(
367
+ f"Skipping {filename} - already being processed via API"
368
+ )
369
+ return None
370
+
371
+ # Compute file hash early for failed file tracking
372
+ current_file_hash = None
373
+ try:
374
+ from gaia.utils import compute_file_hash
375
+
376
+ current_file_hash = compute_file_hash(Path(file_path))
377
+ except Exception:
378
+ pass
379
+
380
+ # Track current processing file globally
381
+ with _processing_lock:
382
+ _current_processing_file = filename
383
+
384
+ DashboardEventHandler.on_processing_started(filename)
385
+
386
+ try:
387
+ # Step 1: Reading file
388
+ DashboardEventHandler.on_processing_step(
389
+ filename, 1, 7, "Reading file", "running"
390
+ )
391
+
392
+ result = original_process(file_path)
393
+
394
+ if result:
395
+ patient_id = result.get("id")
396
+
397
+ # Fetch actual saved patient record from database
398
+ # This ensures SSE event matches database (handles additional_fields, etc.)
399
+ if patient_id and _agent_instance:
400
+ try:
401
+ db_record = _agent_instance.query(
402
+ "SELECT * FROM patients WHERE id = :id",
403
+ {"id": patient_id},
404
+ )
405
+ if db_record:
406
+ # Use database record as base, merge extraction metadata
407
+ event_data = dict(db_record[0])
408
+ # Add extraction-only fields not in DB
409
+ for key in [
410
+ "is_new_patient",
411
+ "changes_detected",
412
+ "processing_time_seconds",
413
+ "estimated_manual_seconds",
414
+ ]:
415
+ if key in result:
416
+ event_data[key] = result[key]
417
+ else:
418
+ event_data = result.copy()
419
+ except Exception as e:
420
+ logger.warning(f"Failed to fetch saved patient: {e}")
421
+ event_data = result.copy()
422
+ else:
423
+ event_data = result.copy()
424
+
425
+ # Unpack additional_fields JSON if present
426
+ if event_data.get("additional_fields"):
427
+ try:
428
+ additional = json.loads(event_data["additional_fields"])
429
+ for key, value in additional.items():
430
+ if key not in event_data:
431
+ event_data[key] = value
432
+ except (json.JSONDecodeError, TypeError):
433
+ pass
434
+
435
+ # Remove large fields from event
436
+ excluded_fields = {
437
+ "raw_extraction",
438
+ "file_content",
439
+ "additional_fields",
440
+ }
441
+ event_data = {
442
+ k: v
443
+ for k, v in event_data.items()
444
+ if k not in excluded_fields
445
+ }
446
+ # Truncate file_hash for display
447
+ if event_data.get("file_hash"):
448
+ event_data["file_hash"] = (
449
+ event_data["file_hash"][:12] + "..."
450
+ )
451
+ DashboardEventHandler.on_patient_created(event_data)
452
+ DashboardEventHandler.on_processing_completed(
453
+ filename, True, patient_id
454
+ )
455
+ elif _duplicate_info["is_duplicate"]:
456
+ # Result is None but duplicate was detected - this is success, not failure
457
+ DashboardEventHandler.on_processing_completed(
458
+ filename,
459
+ True,
460
+ None,
461
+ is_duplicate=True,
462
+ patient_name=_duplicate_info.get("patient_name"),
463
+ )
464
+ else:
465
+ # Result is None - actual extraction failure
466
+ # Track failed file hash for watch folder status
467
+ if current_file_hash:
468
+ with _failed_lock:
469
+ _failed_file_hashes.add(current_file_hash)
470
+ DashboardEventHandler.on_processing_completed(
471
+ filename, False, None
472
+ )
473
+ return result
474
+
475
+ except Exception as e:
476
+ # Track failed file hash for watch folder status
477
+ if current_file_hash:
478
+ with _failed_lock:
479
+ _failed_file_hashes.add(current_file_hash)
480
+ DashboardEventHandler.on_processing_error(
481
+ filename, str(e), "exception"
482
+ )
483
+ DashboardEventHandler.on_processing_completed(filename, False)
484
+ raise
485
+ finally:
486
+ _current_file["name"] = None
487
+ # Clear current processing file
488
+ with _processing_lock:
489
+ _current_processing_file = None
490
+
491
+ _agent_instance._process_intake_form = patched_process
492
+ _agent_instance._get_vlm = patched_get_vlm
493
+ _agent_instance._store_patient = patched_store_patient
494
+
495
+ # Register progress callback for SSE events
496
+ def progress_callback(filename, step_num, total_steps, step_name, status):
497
+ # Track duplicate status for completion event
498
+ if status == "duplicate":
499
+ _duplicate_info["is_duplicate"] = True
500
+ # Extract patient name from step_name (format: "Duplicate - already processed as Name")
501
+ if "already processed as" in step_name:
502
+ _duplicate_info["patient_name"] = step_name.split(
503
+ "already processed as"
504
+ )[-1].strip()
505
+ DashboardEventHandler.on_processing_step(
506
+ filename, step_num, total_steps, step_name, status
507
+ )
508
+
509
+ _agent_instance._progress_callback = progress_callback
510
+
511
+ # Now start file watching (will process existing files)
512
+ logger.info("Starting file watching...")
513
+ # Initialize recent events buffer from existing patients
514
+ # This ensures the live feed is populated even on server restart
515
+ try:
516
+ results = _agent_instance.query(
517
+ """SELECT id, first_name, last_name, is_new_patient,
518
+ processing_time_seconds, source_file, created_at
519
+ FROM patients ORDER BY created_at DESC LIMIT 10"""
520
+ )
521
+ if results:
522
+ with _recent_events_lock:
523
+ for row in reversed(
524
+ results
525
+ ): # Add oldest first so newest is last
526
+ event = {
527
+ "type": "patient_created",
528
+ "timestamp": row.get("created_at", ""),
529
+ "data": {
530
+ "id": row.get("id"),
531
+ "first_name": row.get("first_name"),
532
+ "last_name": row.get("last_name"),
533
+ "is_new_patient": row.get("is_new_patient"),
534
+ "processing_time_seconds": row.get(
535
+ "processing_time_seconds"
536
+ ),
537
+ "source_file": row.get("source_file"),
538
+ "filename": row.get("source_file"),
539
+ },
540
+ }
541
+ _recent_events.append(event)
542
+ logger.info(
543
+ f"Initialized live feed with {len(results)} recent patients"
544
+ )
545
+ except Exception as e:
546
+ logger.warning(f"Could not initialize recent events: {e}")
547
+
548
+ _agent_instance._start_file_watching()
549
+
550
+ logger.info("Agent started in background")
551
+
552
+ # Start agent on startup
553
+ @app.on_event("startup")
554
+ async def startup_event():
555
+ global _main_event_loop
556
+ # Store reference to main event loop for thread-safe SSE broadcasting
557
+ _main_event_loop = asyncio.get_running_loop()
558
+ logger.info("Main event loop captured for SSE broadcasting")
559
+
560
+ thread = threading.Thread(target=start_agent, daemon=True)
561
+ thread.start()
562
+
563
+ @app.on_event("shutdown")
564
+ async def shutdown_event():
565
+ global _agent_instance
566
+ with _agent_lock:
567
+ if _agent_instance:
568
+ _agent_instance.stop()
569
+ _agent_instance = None
570
+
571
+ # API Routes
572
+
573
+ @app.get("/api/patients")
574
+ async def list_patients(
575
+ limit: int = 100,
576
+ offset: int = 0,
577
+ search: Optional[str] = None,
578
+ ) -> Dict[str, Any]:
579
+ """List patients with pagination and search."""
580
+ if not _agent_instance:
581
+ raise HTTPException(status_code=503, detail="Agent not initialized")
582
+
583
+ try:
584
+ # Use basic columns that are guaranteed to exist, handle optional columns gracefully
585
+ # Include changes_detected from most recent intake_session via subquery
586
+ base_query = """
587
+ SELECT p.id, p.first_name, p.last_name, p.date_of_birth, p.phone,
588
+ p.reason_for_visit, p.is_new_patient, p.created_at, p.file_hash,
589
+ p.processing_time_seconds, p.source_file, p.allergies,
590
+ p.insurance_provider, p.gender,
591
+ (SELECT s.changes_detected FROM intake_sessions s
592
+ WHERE s.patient_id = p.id
593
+ ORDER BY s.created_at DESC LIMIT 1) as changes_detected
594
+ FROM patients p
595
+ """
596
+ if search:
597
+ results = _agent_instance.query(
598
+ base_query
599
+ + """
600
+ WHERE p.first_name LIKE :search OR p.last_name LIKE :search
601
+ ORDER BY p.created_at DESC
602
+ LIMIT :limit OFFSET :offset
603
+ """,
604
+ {"search": f"%{search}%", "limit": limit, "offset": offset},
605
+ )
606
+ else:
607
+ results = _agent_instance.query(
608
+ base_query
609
+ + """
610
+ ORDER BY p.created_at DESC
611
+ LIMIT :limit OFFSET :offset
612
+ """,
613
+ {"limit": limit, "offset": offset},
614
+ )
615
+
616
+ # Try to get estimated_manual_seconds if the column exists
617
+ try:
618
+ extended_results = _agent_instance.query(
619
+ "SELECT id, estimated_manual_seconds FROM patients WHERE id IN ("
620
+ + ",".join(str(r["id"]) for r in results)
621
+ + ")"
622
+ if results
623
+ else "SELECT 1 WHERE 0"
624
+ )
625
+ manual_times = {
626
+ r["id"]: r.get("estimated_manual_seconds") for r in extended_results
627
+ }
628
+ for r in results:
629
+ r["estimated_manual_seconds"] = manual_times.get(r["id"])
630
+ except Exception:
631
+ # Column doesn't exist, set to None
632
+ for r in results:
633
+ r["estimated_manual_seconds"] = None
634
+
635
+ # Process results: truncate file_hash and parse changes_detected JSON
636
+ for patient in results:
637
+ if patient.get("file_hash"):
638
+ patient["file_hash"] = patient["file_hash"][:12] + "..."
639
+ # Parse changes_detected from JSON string if present
640
+ if patient.get("changes_detected"):
641
+ try:
642
+ patient["changes_detected"] = json.loads(
643
+ patient["changes_detected"]
644
+ )
645
+ except (json.JSONDecodeError, TypeError):
646
+ patient["changes_detected"] = None
647
+
648
+ count_result = _agent_instance.query(
649
+ "SELECT COUNT(*) as count FROM patients"
650
+ )
651
+ total = count_result[0]["count"] if count_result else 0
652
+
653
+ return {
654
+ "patients": results,
655
+ "total": total,
656
+ "limit": limit,
657
+ "offset": offset,
658
+ }
659
+ except Exception as e:
660
+ logger.error(f"Error listing patients: {e}")
661
+ raise HTTPException(status_code=500, detail=str(e))
662
+
663
+ @app.get("/api/patients/{patient_id}")
664
+ async def get_patient(patient_id: int) -> Dict[str, Any]:
665
+ """Get patient details by ID."""
666
+ if not _agent_instance:
667
+ raise HTTPException(status_code=503, detail="Agent not initialized")
668
+
669
+ try:
670
+ results = _agent_instance.query(
671
+ "SELECT * FROM patients WHERE id = :id",
672
+ {"id": patient_id},
673
+ )
674
+
675
+ if not results:
676
+ raise HTTPException(status_code=404, detail="Patient not found")
677
+
678
+ patient = results[0]
679
+ # Remove large fields from API response
680
+ patient.pop("raw_extraction", None)
681
+ patient.pop("file_content", None)
682
+ # Truncate file_hash for display
683
+ if patient.get("file_hash"):
684
+ patient["file_hash"] = patient["file_hash"][:12] + "..."
685
+ return patient
686
+ except HTTPException:
687
+ raise
688
+ except Exception as e:
689
+ logger.error(f"Error getting patient: {e}")
690
+ raise HTTPException(status_code=500, detail=str(e))
691
+
692
+ @app.put("/api/patients/{patient_id}")
693
+ async def update_patient(
694
+ patient_id: int, request: PatientUpdateRequest
695
+ ) -> Dict[str, Any]:
696
+ """Update patient details. Only provided fields will be updated."""
697
+ if not _agent_instance:
698
+ raise HTTPException(status_code=503, detail="Agent not initialized")
699
+
700
+ try:
701
+ # Check patient exists
702
+ existing = _agent_instance.query(
703
+ "SELECT id, first_name, last_name FROM patients WHERE id = :id",
704
+ {"id": patient_id},
705
+ )
706
+ if not existing:
707
+ raise HTTPException(status_code=404, detail="Patient not found")
708
+
709
+ # Build update data from non-None fields
710
+ update_data = {}
711
+ request_dict = request.model_dump(exclude_unset=True)
712
+
713
+ for field, value in request_dict.items():
714
+ if value is not None:
715
+ update_data[field] = value
716
+
717
+ if not update_data:
718
+ return {
719
+ "success": True,
720
+ "patient_id": patient_id,
721
+ "message": "No fields to update",
722
+ "updated_fields": [],
723
+ }
724
+
725
+ # Add updated_at timestamp
726
+ update_data["updated_at"] = datetime.now().isoformat()
727
+
728
+ # Use mixin's update() method
729
+ _agent_instance.update(
730
+ "patients",
731
+ update_data,
732
+ "id = :id",
733
+ {"id": patient_id},
734
+ )
735
+
736
+ # Check if we need to update/create alerts based on changes
737
+ if "allergies" in update_data and update_data["allergies"]:
738
+ # Check for existing allergy alert
739
+ existing_alert = _agent_instance.query(
740
+ """SELECT id FROM alerts
741
+ WHERE patient_id = :pid AND alert_type = 'allergy'
742
+ AND acknowledged = FALSE""",
743
+ {"pid": patient_id},
744
+ )
745
+ if not existing_alert:
746
+ _agent_instance.insert(
747
+ "alerts",
748
+ {
749
+ "patient_id": patient_id,
750
+ "alert_type": "allergy",
751
+ "priority": "critical",
752
+ "message": f"Patient has allergies: {update_data['allergies']}",
753
+ "data": json.dumps({"allergies": update_data["allergies"]}),
754
+ },
755
+ )
756
+
757
+ # If phone was added, remove missing_field alert if it exists
758
+ if "phone" in update_data and update_data["phone"]:
759
+ _agent_instance.delete(
760
+ "alerts",
761
+ "patient_id = :pid AND alert_type = 'missing_field' "
762
+ "AND message LIKE '%phone%'",
763
+ {"pid": patient_id},
764
+ )
765
+
766
+ logger.info(f"Updated patient {patient_id}: {list(update_data.keys())}")
767
+
768
+ return {
769
+ "success": True,
770
+ "patient_id": patient_id,
771
+ "message": f"Updated {len(update_data)} field(s)",
772
+ "updated_fields": list(update_data.keys()),
773
+ }
774
+
775
+ except HTTPException:
776
+ raise
777
+ except Exception as e:
778
+ logger.error(f"Error updating patient: {e}")
779
+ raise HTTPException(status_code=500, detail=str(e))
780
+
781
+ @app.delete("/api/patients/{patient_id}")
782
+ async def delete_patient(patient_id: int, delete_file: bool = True):
783
+ """Delete a patient, their associated data, and optionally the source file."""
784
+ if not _agent_instance:
785
+ raise HTTPException(status_code=503, detail="Agent not initialized")
786
+
787
+ try:
788
+ # Check patient exists and get source file path
789
+ existing = _agent_instance.query(
790
+ "SELECT id, first_name, last_name, source_file FROM patients WHERE id = :id",
791
+ {"id": patient_id},
792
+ )
793
+ if not existing:
794
+ raise HTTPException(status_code=404, detail="Patient not found")
795
+
796
+ patient = existing[0]
797
+ source_file = patient.get("source_file")
798
+ file_deleted = False
799
+
800
+ # Delete the source file if requested and it exists
801
+ if delete_file and source_file:
802
+ try:
803
+ source_path = Path(source_file)
804
+ if source_path.exists():
805
+ source_path.unlink()
806
+ file_deleted = True
807
+ logger.info(f"Deleted source file: {source_file}")
808
+ except Exception as e:
809
+ logger.warning(f"Could not delete source file {source_file}: {e}")
810
+
811
+ # Delete associated alerts first
812
+ _agent_instance.delete("alerts", "patient_id = :id", {"id": patient_id})
813
+
814
+ # Delete associated sessions
815
+ _agent_instance.delete(
816
+ "intake_sessions", "patient_id = :id", {"id": patient_id}
817
+ )
818
+
819
+ # Delete patient
820
+ _agent_instance.delete("patients", "id = :id", {"id": patient_id})
821
+
822
+ message = f"Deleted patient {patient['first_name']} {patient['last_name']}"
823
+ if file_deleted:
824
+ message += " and source file"
825
+
826
+ return {
827
+ "success": True,
828
+ "message": message,
829
+ "patient_id": patient_id,
830
+ "file_deleted": file_deleted,
831
+ }
832
+ except HTTPException:
833
+ raise
834
+ except Exception as e:
835
+ logger.error(f"Error deleting patient: {e}")
836
+ raise HTTPException(status_code=500, detail=str(e))
837
+
838
+ @app.post("/api/patients/{patient_id}/mark-reviewed")
839
+ async def mark_patient_reviewed(patient_id: int) -> Dict[str, Any]:
840
+ """
841
+ Mark a patient's changes as reviewed, clearing the pending review status.
842
+
843
+ This clears the changes_detected field in the most recent intake session
844
+ for the specified patient.
845
+ """
846
+ if not _agent_instance:
847
+ raise HTTPException(status_code=503, detail="Agent not initialized")
848
+
849
+ try:
850
+ # Check patient exists
851
+ existing = _agent_instance.query(
852
+ "SELECT id, first_name, last_name FROM patients WHERE id = :id",
853
+ {"id": patient_id},
854
+ )
855
+ if not existing:
856
+ raise HTTPException(status_code=404, detail="Patient not found")
857
+
858
+ patient = existing[0]
859
+
860
+ # Clear changes_detected in the most recent intake session
861
+ latest_session = _agent_instance.query(
862
+ """SELECT id FROM intake_sessions
863
+ WHERE patient_id = :pid
864
+ ORDER BY created_at DESC LIMIT 1""",
865
+ {"pid": patient_id},
866
+ one=True,
867
+ )
868
+ if latest_session:
869
+ _agent_instance.update(
870
+ "intake_sessions",
871
+ {"changes_detected": None},
872
+ "id = :id",
873
+ {"id": latest_session["id"]},
874
+ )
875
+
876
+ logger.info(f"Marked patient {patient_id} as reviewed")
877
+
878
+ return {
879
+ "success": True,
880
+ "patient_id": patient_id,
881
+ "patient_name": f"{patient.get('first_name', '')} {patient.get('last_name', '')}".strip(),
882
+ "message": "Patient marked as reviewed",
883
+ }
884
+
885
+ except HTTPException:
886
+ raise
887
+ except Exception as e:
888
+ logger.error(f"Error marking patient as reviewed: {e}")
889
+ raise HTTPException(status_code=500, detail=str(e))
890
+
891
+ @app.get("/api/patients/{patient_id}/file")
892
+ async def download_patient_file(patient_id: int, inline: bool = False):
893
+ """Download or view the original intake form file for a patient."""
894
+ if not _agent_instance:
895
+ raise HTTPException(status_code=503, detail="Agent not initialized")
896
+
897
+ try:
898
+ results = _agent_instance.query(
899
+ "SELECT file_content, source_file FROM patients WHERE id = :id",
900
+ {"id": patient_id},
901
+ )
902
+
903
+ if not results:
904
+ raise HTTPException(status_code=404, detail="Patient not found")
905
+
906
+ patient = results[0]
907
+ file_content = patient.get("file_content")
908
+ source_file = patient.get("source_file", "intake_form")
909
+
910
+ if not file_content:
911
+ raise HTTPException(
912
+ status_code=404,
913
+ detail="Original file not available (older record)",
914
+ )
915
+
916
+ # Determine MIME type from filename
917
+ filename = Path(source_file).name if source_file else "intake_form"
918
+ suffix = Path(source_file).suffix.lower() if source_file else ""
919
+ mime_types = {
920
+ ".pdf": "application/pdf",
921
+ ".png": "image/png",
922
+ ".jpg": "image/jpeg",
923
+ ".jpeg": "image/jpeg",
924
+ ".tiff": "image/tiff",
925
+ ".bmp": "image/bmp",
926
+ }
927
+ media_type = mime_types.get(suffix, "application/octet-stream")
928
+
929
+ disposition = "inline" if inline else "attachment"
930
+ return Response(
931
+ content=file_content,
932
+ media_type=media_type,
933
+ headers={
934
+ "Content-Disposition": f'{disposition}; filename="{filename}"',
935
+ },
936
+ )
937
+ except HTTPException:
938
+ raise
939
+ except Exception as e:
940
+ logger.error(f"Error downloading file: {e}")
941
+ raise HTTPException(status_code=500, detail=str(e))
942
+
943
+ @app.get("/api/stats")
944
+ async def get_stats() -> Dict[str, Any]:
945
+ """Get processing statistics."""
946
+ if not _agent_instance:
947
+ raise HTTPException(status_code=503, detail="Agent not initialized")
948
+
949
+ try:
950
+ return _agent_instance.get_stats()
951
+ except Exception as e:
952
+ logger.error(f"Error getting stats: {e}")
953
+ raise HTTPException(status_code=500, detail=str(e))
954
+
955
+ @app.get("/api/alerts")
956
+ async def list_alerts(
957
+ unacknowledged_only: bool = True,
958
+ limit: int = 50,
959
+ ) -> Dict[str, Any]:
960
+ """List alerts with optional filtering."""
961
+ if not _agent_instance:
962
+ raise HTTPException(status_code=503, detail="Agent not initialized")
963
+
964
+ try:
965
+ if unacknowledged_only:
966
+ results = _agent_instance.query(
967
+ """
968
+ SELECT a.*, p.first_name, p.last_name
969
+ FROM alerts a
970
+ LEFT JOIN patients p ON a.patient_id = p.id
971
+ WHERE a.acknowledged = FALSE
972
+ ORDER BY
973
+ CASE a.priority
974
+ WHEN 'critical' THEN 1
975
+ WHEN 'high' THEN 2
976
+ WHEN 'medium' THEN 3
977
+ ELSE 4
978
+ END,
979
+ a.created_at DESC
980
+ LIMIT :limit
981
+ """,
982
+ {"limit": limit},
983
+ )
984
+ else:
985
+ results = _agent_instance.query(
986
+ """
987
+ SELECT a.*, p.first_name, p.last_name
988
+ FROM alerts a
989
+ LEFT JOIN patients p ON a.patient_id = p.id
990
+ ORDER BY a.created_at DESC
991
+ LIMIT :limit
992
+ """,
993
+ {"limit": limit},
994
+ )
995
+
996
+ return {"alerts": results, "count": len(results)}
997
+ except Exception as e:
998
+ logger.error(f"Error listing alerts: {e}")
999
+ raise HTTPException(status_code=500, detail=str(e))
1000
+
1001
+ @app.post("/api/alerts/{alert_id}/acknowledge")
1002
+ async def acknowledge_alert(
1003
+ alert_id: int,
1004
+ acknowledged_by: str = "Staff",
1005
+ ) -> Dict[str, Any]:
1006
+ """Acknowledge an alert."""
1007
+ if not _agent_instance:
1008
+ raise HTTPException(status_code=503, detail="Agent not initialized")
1009
+
1010
+ try:
1011
+ # Check alert exists
1012
+ existing = _agent_instance.query(
1013
+ "SELECT id FROM alerts WHERE id = :id",
1014
+ {"id": alert_id},
1015
+ )
1016
+ if not existing:
1017
+ raise HTTPException(status_code=404, detail="Alert not found")
1018
+
1019
+ # Acknowledge using proper update method
1020
+ _agent_instance.update(
1021
+ "alerts",
1022
+ {
1023
+ "acknowledged": True,
1024
+ "acknowledged_by": acknowledged_by,
1025
+ "acknowledged_at": datetime.now().isoformat(),
1026
+ },
1027
+ "id = :id",
1028
+ {"id": alert_id},
1029
+ )
1030
+
1031
+ return {"success": True, "alert_id": alert_id}
1032
+ except HTTPException:
1033
+ raise
1034
+ except Exception as e:
1035
+ logger.error(f"Error acknowledging alert: {e}")
1036
+ raise HTTPException(status_code=500, detail=str(e))
1037
+
1038
+ @app.get("/api/sessions")
1039
+ async def list_sessions(limit: int = 50) -> Dict[str, Any]:
1040
+ """List recent intake sessions for audit trail."""
1041
+ if not _agent_instance:
1042
+ raise HTTPException(status_code=503, detail="Agent not initialized")
1043
+
1044
+ try:
1045
+ results = _agent_instance.query(
1046
+ """
1047
+ SELECT s.*, p.first_name, p.last_name
1048
+ FROM intake_sessions s
1049
+ LEFT JOIN patients p ON s.patient_id = p.id
1050
+ ORDER BY s.created_at DESC
1051
+ LIMIT :limit
1052
+ """,
1053
+ {"limit": limit},
1054
+ )
1055
+ return {"sessions": results, "count": len(results)}
1056
+ except Exception as e:
1057
+ logger.error(f"Error listing sessions: {e}")
1058
+ raise HTTPException(status_code=500, detail=str(e))
1059
+
1060
+ @app.get("/api/events")
1061
+ async def event_stream():
1062
+ """SSE endpoint for real-time updates."""
1063
+ client_queue: asyncio.Queue = asyncio.Queue(maxsize=100)
1064
+
1065
+ async with _sse_clients_lock:
1066
+ _sse_clients.append(client_queue)
1067
+
1068
+ # Get recent events to replay to this new client
1069
+ with _recent_events_lock:
1070
+ events_to_replay = list(_recent_events)
1071
+
1072
+ async def generate():
1073
+ """Generate SSE events."""
1074
+ last_heartbeat = time.time()
1075
+
1076
+ try:
1077
+ # First, replay recent events to populate the feed for new clients
1078
+ for event in events_to_replay:
1079
+ yield f"data: {_safe_json_dumps(event)}\n\n"
1080
+
1081
+ while True:
1082
+ try:
1083
+ # Wait for event with timeout
1084
+ try:
1085
+ event = await asyncio.wait_for(
1086
+ client_queue.get(), timeout=1.0
1087
+ )
1088
+ yield f"data: {_safe_json_dumps(event)}\n\n"
1089
+ except asyncio.TimeoutError:
1090
+ pass
1091
+
1092
+ # Send heartbeat every 30 seconds
1093
+ current_time = time.time()
1094
+ if current_time - last_heartbeat > 30:
1095
+ yield f"data: {_safe_json_dumps({'type': 'heartbeat'})}\n\n"
1096
+ last_heartbeat = current_time
1097
+
1098
+ except Exception as e:
1099
+ logger.error(f"Error in SSE stream: {e}")
1100
+ break
1101
+ finally:
1102
+ # Remove client on disconnect
1103
+ async with _sse_clients_lock:
1104
+ if client_queue in _sse_clients:
1105
+ _sse_clients.remove(client_queue)
1106
+
1107
+ return StreamingResponse(
1108
+ generate(),
1109
+ media_type="text/event-stream",
1110
+ headers={
1111
+ "Cache-Control": "no-cache",
1112
+ "Connection": "keep-alive",
1113
+ "X-Accel-Buffering": "no",
1114
+ },
1115
+ )
1116
+
1117
+ @app.get("/api/health")
1118
+ async def health_check() -> Dict[str, Any]:
1119
+ """Health check endpoint."""
1120
+ return {
1121
+ "status": "healthy",
1122
+ "agent_running": _agent_instance is not None,
1123
+ "connected_clients": len(_sse_clients),
1124
+ "timestamp": datetime.now().isoformat(),
1125
+ }
1126
+
1127
+ @app.post("/api/chat")
1128
+ async def chat(request: ChatRequest) -> Dict[str, Any]:
1129
+ """
1130
+ Chat with the agent using natural language.
1131
+
1132
+ Send queries like:
1133
+ - "How many patients were processed today?"
1134
+ - "Find patient John Smith"
1135
+ - "Show me patients with allergies"
1136
+ - "What are the statistics?"
1137
+ """
1138
+ if not _agent_instance:
1139
+ raise HTTPException(status_code=503, detail="Agent not initialized")
1140
+
1141
+ try:
1142
+ # Process the query through the agent
1143
+ result = _agent_instance.process_query(request.message)
1144
+
1145
+ # Extract the response text
1146
+ response_text = ""
1147
+ if isinstance(result, dict):
1148
+ response_text = result.get("result", str(result))
1149
+ else:
1150
+ response_text = str(result) if result else "No response generated."
1151
+
1152
+ return {
1153
+ "success": True,
1154
+ "message": request.message,
1155
+ "response": response_text,
1156
+ "timestamp": datetime.now().isoformat(),
1157
+ }
1158
+ except Exception as e:
1159
+ logger.error(f"Error processing chat: {e}")
1160
+ return {
1161
+ "success": False,
1162
+ "message": request.message,
1163
+ "response": f"Error processing your request: {str(e)}",
1164
+ "timestamp": datetime.now().isoformat(),
1165
+ }
1166
+
1167
+ @app.get("/api/config")
1168
+ async def get_config() -> Dict[str, Any]:
1169
+ """Get current agent configuration with full resolved paths."""
1170
+ # Resolve full paths
1171
+ watch_path = Path(watch_dir).resolve()
1172
+ db_full_path = Path(db_path).resolve()
1173
+
1174
+ if not _agent_instance:
1175
+ return {
1176
+ "watch_dir": str(watch_path),
1177
+ "watch_dir_relative": watch_dir,
1178
+ "db_path": str(db_full_path),
1179
+ "db_path_relative": db_path,
1180
+ "agent_running": False,
1181
+ "vlm_model": "Qwen3-VL-4B-Instruct-GGUF",
1182
+ }
1183
+
1184
+ return {
1185
+ "watch_dir": str(Path(_agent_instance._watch_dir).resolve()),
1186
+ "watch_dir_relative": str(_agent_instance._watch_dir),
1187
+ "db_path": str(Path(_agent_instance._db_path).resolve()),
1188
+ "db_path_relative": str(_agent_instance._db_path),
1189
+ "agent_running": True,
1190
+ "vlm_model": _agent_instance._vlm_model,
1191
+ }
1192
+
1193
+ @app.get("/api/init/status")
1194
+ async def get_init_status() -> Dict[str, Any]:
1195
+ """Check all required model initialization status with context size info."""
1196
+ REQUIRED_CONTEXT_SIZE = 32768
1197
+
1198
+ # Required models for EMR agent
1199
+ vlm_model = "Qwen3-VL-4B-Instruct-GGUF"
1200
+ llm_model = "Qwen3-Coder-30B-A3B-Instruct-GGUF"
1201
+ embed_model = "nomic-embed-text-v2-moe-GGUF"
1202
+
1203
+ try:
1204
+ from gaia.llm.lemonade_client import LemonadeClient
1205
+
1206
+ client = LemonadeClient(model=vlm_model)
1207
+
1208
+ # Check server health and context size
1209
+ try:
1210
+ health = client.health_check()
1211
+ server_running = health.get("status") == "ok"
1212
+ context_size = health.get("context_size", 0)
1213
+ except Exception:
1214
+ return {
1215
+ "initialized": False,
1216
+ "server_running": False,
1217
+ "context_size": 0,
1218
+ "context_size_ok": False,
1219
+ "models": {
1220
+ "vlm": {"name": vlm_model, "available": False, "loaded": False},
1221
+ "llm": {"name": llm_model, "available": False, "loaded": False},
1222
+ "embedding": {
1223
+ "name": embed_model,
1224
+ "available": False,
1225
+ "loaded": False,
1226
+ },
1227
+ },
1228
+ "ready_count": 0,
1229
+ "total_models": 3,
1230
+ "message": "Lemonade server not running",
1231
+ }
1232
+
1233
+ # Check if models are available (downloaded)
1234
+ models_response = client.list_models()
1235
+ all_models = models_response.get("data", [])
1236
+ available_model_ids = [m.get("id", "") for m in all_models]
1237
+
1238
+ vlm_available = vlm_model in available_model_ids
1239
+ llm_available = llm_model in available_model_ids
1240
+ embed_available = embed_model in available_model_ids
1241
+
1242
+ # Check if models are loaded using check_model_loaded
1243
+ vlm_loaded = client.check_model_loaded(vlm_model)
1244
+ llm_loaded = client.check_model_loaded(llm_model)
1245
+ embed_loaded = client.check_model_loaded(embed_model)
1246
+
1247
+ # Categorize all downloaded models for inventory
1248
+ vlm_models = []
1249
+ llm_models = []
1250
+ embed_models = []
1251
+
1252
+ for m in all_models:
1253
+ model_id = m.get("id", "")
1254
+ model_lower = model_id.lower()
1255
+ if (
1256
+ "vl" in model_lower
1257
+ or "vision" in model_lower
1258
+ or "vlm" in model_lower
1259
+ ):
1260
+ vlm_models.append(model_id)
1261
+ elif (
1262
+ "embed" in model_lower
1263
+ or "bge" in model_lower
1264
+ or "e5" in model_lower
1265
+ or "nomic" in model_lower
1266
+ ):
1267
+ embed_models.append(model_id)
1268
+ else:
1269
+ llm_models.append(model_id)
1270
+
1271
+ # Count ready models
1272
+ ready_count = sum([vlm_loaded, llm_loaded, embed_loaded])
1273
+ context_size_ok = context_size >= REQUIRED_CONTEXT_SIZE
1274
+
1275
+ # Build status message
1276
+ if ready_count == 3 and context_size_ok:
1277
+ message = "All models ready"
1278
+ elif ready_count == 3:
1279
+ message = f"All models ready (context size: {context_size:,}, recommended: {REQUIRED_CONTEXT_SIZE:,})"
1280
+ elif ready_count > 0:
1281
+ message = f"{ready_count}/3 models ready"
1282
+ elif vlm_available or llm_available or embed_available:
1283
+ message = "Models not loaded"
1284
+ else:
1285
+ message = "Models not downloaded"
1286
+
1287
+ return {
1288
+ "initialized": vlm_loaded, # VLM is critical for form processing
1289
+ "server_running": server_running,
1290
+ "context_size": context_size,
1291
+ "context_size_ok": context_size_ok,
1292
+ "required_context_size": REQUIRED_CONTEXT_SIZE,
1293
+ "models": {
1294
+ "vlm": {
1295
+ "name": vlm_model,
1296
+ "available": vlm_available,
1297
+ "loaded": vlm_loaded,
1298
+ "purpose": "Form extraction",
1299
+ },
1300
+ "llm": {
1301
+ "name": llm_model,
1302
+ "available": llm_available,
1303
+ "loaded": llm_loaded,
1304
+ "purpose": "Chat/query processing",
1305
+ },
1306
+ "embedding": {
1307
+ "name": embed_model,
1308
+ "available": embed_available,
1309
+ "loaded": embed_loaded,
1310
+ "purpose": "Similarity search",
1311
+ },
1312
+ },
1313
+ "ready_count": ready_count,
1314
+ "total_models": 3,
1315
+ "model_inventory": {
1316
+ "vlm": vlm_models[:3],
1317
+ "llm": llm_models[:3],
1318
+ "embedding": embed_models[:3],
1319
+ "total": len(all_models),
1320
+ },
1321
+ "message": message,
1322
+ }
1323
+ except Exception as e:
1324
+ logger.error(f"Error checking init status: {e}")
1325
+ return {
1326
+ "initialized": False,
1327
+ "server_running": False,
1328
+ "context_size": 0,
1329
+ "context_size_ok": False,
1330
+ "models": {
1331
+ "vlm": {"name": vlm_model, "available": False, "loaded": False},
1332
+ "llm": {"name": llm_model, "available": False, "loaded": False},
1333
+ "embedding": {
1334
+ "name": embed_model,
1335
+ "available": False,
1336
+ "loaded": False,
1337
+ },
1338
+ },
1339
+ "ready_count": 0,
1340
+ "total_models": 3,
1341
+ "message": f"Error: {str(e)}",
1342
+ }
1343
+
1344
+ @app.post("/api/init")
1345
+ async def run_init() -> Dict[str, Any]:
1346
+ """
1347
+ Initialize all required models (VLM, LLM, Embedding).
1348
+
1349
+ This runs the equivalent of `gaia-emr init` from the dashboard.
1350
+ """
1351
+ try:
1352
+ from gaia.llm.lemonade_client import LemonadeClient
1353
+
1354
+ # Required models for EMR agent
1355
+ vlm_model = "Qwen3-VL-4B-Instruct-GGUF"
1356
+ llm_model = "Qwen3-Coder-30B-A3B-Instruct-GGUF"
1357
+ embed_model = "nomic-embed-text-v2-moe-GGUF"
1358
+
1359
+ required_models = [
1360
+ ("VLM", vlm_model),
1361
+ ("LLM", llm_model),
1362
+ ("Embedding", embed_model),
1363
+ ]
1364
+
1365
+ steps = []
1366
+
1367
+ # Step 1: Check server
1368
+ steps.append(
1369
+ {"step": 1, "name": "Checking Lemonade server", "status": "running"}
1370
+ )
1371
+
1372
+ client = LemonadeClient(model=vlm_model)
1373
+
1374
+ try:
1375
+ health = client.health_check()
1376
+ if health.get("status") != "ok":
1377
+ return {
1378
+ "success": False,
1379
+ "message": "Lemonade server not healthy",
1380
+ "steps": steps,
1381
+ }
1382
+ steps[-1]["status"] = "complete"
1383
+ except Exception as e:
1384
+ steps[-1]["status"] = "error"
1385
+ return {
1386
+ "success": False,
1387
+ "message": f"Lemonade server not running: {str(e)}",
1388
+ "steps": steps,
1389
+ }
1390
+
1391
+ # Step 2: Load all models
1392
+ step_num = 2
1393
+ for model_type, model_name in required_models:
1394
+ steps.append(
1395
+ {
1396
+ "step": step_num,
1397
+ "name": f"Loading {model_type}: {model_name}",
1398
+ "status": "running",
1399
+ }
1400
+ )
1401
+
1402
+ try:
1403
+ await asyncio.to_thread(
1404
+ client.load_model,
1405
+ model_name,
1406
+ 1800,
1407
+ True, # timeout, auto_download
1408
+ )
1409
+ steps[-1]["status"] = "complete"
1410
+ except Exception as e:
1411
+ steps[-1]["status"] = "warning"
1412
+ steps[-1]["error"] = str(e)[:50]
1413
+
1414
+ step_num += 1
1415
+
1416
+ # Verify models
1417
+ steps.append(
1418
+ {"step": step_num, "name": "Verifying models", "status": "running"}
1419
+ )
1420
+
1421
+ vlm_ready = client.check_model_loaded(vlm_model)
1422
+ llm_ready = client.check_model_loaded(llm_model)
1423
+ embed_ready = client.check_model_loaded(embed_model)
1424
+
1425
+ ready_count = sum([vlm_ready, llm_ready, embed_ready])
1426
+ steps[-1]["status"] = "complete"
1427
+
1428
+ if vlm_ready: # VLM is critical
1429
+ return {
1430
+ "success": True,
1431
+ "message": f"Initialized ({ready_count}/3 models ready)",
1432
+ "ready_count": ready_count,
1433
+ "steps": steps,
1434
+ }
1435
+ else:
1436
+ return {
1437
+ "success": False,
1438
+ "message": "VLM model failed to load - form processing will not work",
1439
+ "ready_count": ready_count,
1440
+ "steps": steps,
1441
+ }
1442
+
1443
+ except Exception as e:
1444
+ logger.error(f"Error during init: {e}")
1445
+ return {
1446
+ "success": False,
1447
+ "message": f"Initialization failed: {str(e)}",
1448
+ "steps": steps if "steps" in dir() else [],
1449
+ }
1450
+
1451
+ @app.get("/api/watch-folder")
1452
+ async def get_watch_folder_files() -> Dict[str, Any]:
1453
+ """
1454
+ Get list of files in the watch folder with their processing status.
1455
+
1456
+ Returns files with status:
1457
+ - 'queued': File exists but hasn't been processed yet (orange dot)
1458
+ - 'processing': Currently being processed (flashing red dot)
1459
+ - 'processed': Successfully processed (green dot)
1460
+ """
1461
+ if not _agent_instance:
1462
+ return {
1463
+ "watch_dir": str(watch_dir) if watch_dir else "Not configured",
1464
+ "files": [],
1465
+ "total": 0,
1466
+ "processed_count": 0,
1467
+ "pending_count": 0,
1468
+ }
1469
+
1470
+ try:
1471
+ from gaia.utils import compute_file_hash
1472
+
1473
+ watch_path = Path(_agent_instance._watch_dir)
1474
+
1475
+ if not watch_path.exists():
1476
+ return {
1477
+ "watch_dir": str(watch_path),
1478
+ "files": [],
1479
+ "total": 0,
1480
+ "processed_count": 0,
1481
+ "pending_count": 0,
1482
+ "error": "Watch folder does not exist",
1483
+ }
1484
+
1485
+ # Get all file hashes from database to check processed status
1486
+ processed_hashes = {}
1487
+ try:
1488
+ db_results = _agent_instance.query(
1489
+ "SELECT file_hash, id, first_name, last_name, created_at FROM patients WHERE file_hash IS NOT NULL"
1490
+ )
1491
+ for row in db_results:
1492
+ processed_hashes[row["file_hash"]] = {
1493
+ "patient_id": row["id"],
1494
+ "patient_name": f"{row.get('first_name', '')} {row.get('last_name', '')}".strip(),
1495
+ "processed_at": row.get("created_at"),
1496
+ }
1497
+ except Exception as e:
1498
+ logger.warning(f"Could not query processed hashes: {e}")
1499
+
1500
+ # Get current processing file
1501
+ with _processing_lock:
1502
+ current_file = _current_processing_file
1503
+
1504
+ # Get failed file hashes
1505
+ with _failed_lock:
1506
+ failed_hashes = set(_failed_file_hashes)
1507
+
1508
+ # Supported file extensions
1509
+ supported_extensions = {".jpg", ".jpeg", ".png", ".pdf", ".tiff", ".bmp"}
1510
+
1511
+ files = []
1512
+ processed_count = 0
1513
+ queued_count = 0
1514
+ processing_count = 0
1515
+ failed_count = 0
1516
+
1517
+ for file_path in watch_path.iterdir():
1518
+ if not file_path.is_file():
1519
+ continue
1520
+
1521
+ suffix = file_path.suffix.lower()
1522
+ if suffix not in supported_extensions:
1523
+ continue
1524
+
1525
+ try:
1526
+ stat = file_path.stat()
1527
+ file_hash = compute_file_hash(str(file_path))
1528
+
1529
+ # Determine status
1530
+ if file_path.name == current_file:
1531
+ status = "processing"
1532
+ processing_count += 1
1533
+ elif file_hash and file_hash in processed_hashes:
1534
+ status = "processed"
1535
+ processed_count += 1
1536
+ elif file_hash and file_hash in failed_hashes:
1537
+ status = "failed"
1538
+ failed_count += 1
1539
+ else:
1540
+ status = "queued"
1541
+ queued_count += 1
1542
+
1543
+ file_info = {
1544
+ "name": file_path.name,
1545
+ "path": str(file_path),
1546
+ "size": stat.st_size,
1547
+ "size_formatted": _format_file_size(stat.st_size),
1548
+ "modified": datetime.fromtimestamp(stat.st_mtime).isoformat(),
1549
+ "modified_formatted": datetime.fromtimestamp(
1550
+ stat.st_mtime
1551
+ ).strftime("%Y-%m-%d %H:%M"),
1552
+ "extension": suffix,
1553
+ "status": status,
1554
+ "hash": file_hash[:12] + "..." if file_hash else None,
1555
+ }
1556
+
1557
+ # Add patient info if processed
1558
+ if status == "processed" and file_hash in processed_hashes:
1559
+ file_info["patient_id"] = processed_hashes[file_hash][
1560
+ "patient_id"
1561
+ ]
1562
+ file_info["patient_name"] = processed_hashes[file_hash][
1563
+ "patient_name"
1564
+ ]
1565
+ file_info["processed_at"] = processed_hashes[file_hash][
1566
+ "processed_at"
1567
+ ]
1568
+
1569
+ files.append(file_info)
1570
+
1571
+ except (OSError, IOError) as e:
1572
+ logger.warning(f"Could not read file {file_path}: {e}")
1573
+ continue
1574
+
1575
+ # Sort files: processing first, then failed, then queued, then processed
1576
+ status_order = {"processing": 0, "failed": 1, "queued": 2, "processed": 3}
1577
+ files.sort(
1578
+ key=lambda f: (status_order.get(f["status"], 4), f.get("modified", ""))
1579
+ )
1580
+
1581
+ return {
1582
+ "watch_dir": str(watch_path),
1583
+ "files": files,
1584
+ "total": len(files),
1585
+ "processed_count": processed_count,
1586
+ "queued_count": queued_count,
1587
+ "processing_count": processing_count,
1588
+ "failed_count": failed_count,
1589
+ "pending_count": queued_count + processing_count, # backwards compat
1590
+ "current_processing": current_file,
1591
+ }
1592
+
1593
+ except Exception as e:
1594
+ logger.error(f"Error getting watch folder files: {e}")
1595
+ raise HTTPException(status_code=500, detail=str(e))
1596
+
1597
+ def _format_file_size(size_bytes: int) -> str:
1598
+ """Format file size in human-readable format."""
1599
+ if size_bytes < 1024:
1600
+ return f"{size_bytes} B"
1601
+ elif size_bytes < 1024 * 1024:
1602
+ return f"{size_bytes / 1024:.1f} KB"
1603
+ else:
1604
+ return f"{size_bytes / (1024 * 1024):.1f} MB"
1605
+
1606
+ @app.put("/api/config/watch-dir")
1607
+ async def update_watch_dir(config: WatchDirConfig) -> Dict[str, Any]:
1608
+ """Update the watch directory."""
1609
+ if not _agent_instance:
1610
+ raise HTTPException(status_code=503, detail="Agent not initialized")
1611
+
1612
+ new_dir = Path(config.watch_dir).expanduser().resolve()
1613
+
1614
+ try:
1615
+ # Create directory if it doesn't exist
1616
+ new_dir.mkdir(parents=True, exist_ok=True)
1617
+
1618
+ # Stop existing watchers
1619
+ _agent_instance.stop_all_watchers()
1620
+
1621
+ # Update watch directory
1622
+ _agent_instance._watch_dir = new_dir
1623
+
1624
+ # Restart file watching
1625
+ _agent_instance._start_file_watching()
1626
+
1627
+ logger.info(f"Watch directory updated to: {new_dir}")
1628
+
1629
+ return {
1630
+ "success": True,
1631
+ "watch_dir": str(new_dir),
1632
+ "message": f"Now watching: {new_dir}",
1633
+ }
1634
+ except Exception as e:
1635
+ logger.error(f"Failed to update watch directory: {e}")
1636
+ raise HTTPException(status_code=400, detail=str(e))
1637
+
1638
+ @app.post("/api/upload")
1639
+ async def upload_file(file: UploadFile = File(...)) -> Dict[str, Any]:
1640
+ """Upload and process an intake form file."""
1641
+ if not _agent_instance:
1642
+ raise HTTPException(status_code=503, detail="Agent not initialized")
1643
+
1644
+ # Validate filename
1645
+ if not file.filename:
1646
+ raise HTTPException(status_code=400, detail="No filename provided")
1647
+
1648
+ # Validate file type
1649
+ allowed_extensions = {".png", ".jpg", ".jpeg", ".pdf", ".tiff", ".bmp"}
1650
+ suffix = Path(file.filename).suffix.lower()
1651
+
1652
+ if suffix not in allowed_extensions:
1653
+ raise HTTPException(
1654
+ status_code=400,
1655
+ detail=f"Unsupported file type: {suffix}. Allowed: {', '.join(allowed_extensions)}",
1656
+ )
1657
+
1658
+ try:
1659
+ # Read file content
1660
+ content = await file.read()
1661
+
1662
+ if not content:
1663
+ raise HTTPException(status_code=400, detail="Empty file uploaded")
1664
+
1665
+ # Sanitize filename (remove path components, keep only the basename)
1666
+ safe_filename = Path(file.filename).name
1667
+
1668
+ # Ensure watch directory exists
1669
+ _agent_instance._watch_dir.mkdir(parents=True, exist_ok=True)
1670
+
1671
+ # Add to API processing set BEFORE saving file to prevent race condition
1672
+ # with file watcher detecting the file before we add it to the set
1673
+ with _api_processing_lock:
1674
+ _api_processing_files.add(safe_filename)
1675
+
1676
+ # Save file to watch directory
1677
+ file_path = _agent_instance._watch_dir / safe_filename
1678
+
1679
+ with open(file_path, "wb") as f:
1680
+ f.write(content)
1681
+
1682
+ logger.info(f"File uploaded: {file_path} ({len(content)} bytes)")
1683
+
1684
+ # Check if file is a duplicate before processing
1685
+ from gaia.utils import compute_file_hash
1686
+
1687
+ file_hash = compute_file_hash(str(file_path))
1688
+ if file_hash:
1689
+ existing = _agent_instance.query(
1690
+ "SELECT id, first_name, last_name FROM patients WHERE file_hash = ?",
1691
+ (file_hash,),
1692
+ )
1693
+ if existing:
1694
+ # Clean up from set since we're returning early
1695
+ with _api_processing_lock:
1696
+ _api_processing_files.discard(safe_filename)
1697
+ patient = existing[0]
1698
+ return {
1699
+ "success": True,
1700
+ "filename": safe_filename,
1701
+ "patient_id": patient.get("id"),
1702
+ "patient_name": f"{patient.get('first_name', '')} {patient.get('last_name', '')}".strip(),
1703
+ "is_duplicate": True,
1704
+ "message": "File already processed - showing existing patient",
1705
+ }
1706
+
1707
+ def process_with_flag(fp):
1708
+ """Process file with thread-local flag to mark as API call."""
1709
+ _thread_local.is_api_call = True
1710
+ try:
1711
+ return _agent_instance._process_intake_form(fp)
1712
+ finally:
1713
+ _thread_local.is_api_call = False
1714
+
1715
+ try:
1716
+ # Process the file in a thread pool to avoid blocking the event loop
1717
+ # This allows SSE events to be sent in real-time during processing
1718
+ result = await asyncio.to_thread(process_with_flag, str(file_path))
1719
+ finally:
1720
+ # Remove from API processing set
1721
+ with _api_processing_lock:
1722
+ _api_processing_files.discard(safe_filename)
1723
+
1724
+ if result:
1725
+ return {
1726
+ "success": True,
1727
+ "filename": safe_filename,
1728
+ "patient_id": result.get("id"),
1729
+ "patient_name": f"{result.get('first_name', '')} {result.get('last_name', '')}".strip(),
1730
+ "is_new_patient": result.get("is_new_patient", True),
1731
+ "message": "File processed successfully",
1732
+ }
1733
+ else:
1734
+ return {
1735
+ "success": False,
1736
+ "filename": safe_filename,
1737
+ "message": "Extraction failed - check if form is filled out correctly",
1738
+ }
1739
+ except HTTPException:
1740
+ # Clean up from set on error (safe_filename may not be defined if early error)
1741
+ try:
1742
+ with _api_processing_lock:
1743
+ _api_processing_files.discard(safe_filename)
1744
+ except NameError:
1745
+ pass
1746
+ raise
1747
+ except Exception as e:
1748
+ # Clean up from set on error (safe_filename may not be defined if early error)
1749
+ try:
1750
+ with _api_processing_lock:
1751
+ _api_processing_files.discard(safe_filename)
1752
+ except NameError:
1753
+ pass
1754
+ logger.error(f"Error uploading file: {e}", exc_info=True)
1755
+ raise HTTPException(status_code=500, detail=str(e))
1756
+
1757
+ @app.post("/api/upload-path")
1758
+ async def upload_file_by_path(request: Dict[str, Any]) -> Dict[str, Any]:
1759
+ """Process a file by path (for Electron drag-drop support)."""
1760
+ if not _agent_instance:
1761
+ raise HTTPException(status_code=503, detail="Agent not initialized")
1762
+
1763
+ file_path = request.get("file_path")
1764
+ if not file_path:
1765
+ raise HTTPException(status_code=400, detail="No file_path provided")
1766
+
1767
+ source_path = Path(file_path)
1768
+
1769
+ if not source_path.exists():
1770
+ raise HTTPException(status_code=400, detail=f"File not found: {file_path}")
1771
+
1772
+ # Validate file type
1773
+ allowed_extensions = {".png", ".jpg", ".jpeg", ".pdf", ".tiff", ".bmp"}
1774
+ suffix = source_path.suffix.lower()
1775
+
1776
+ if suffix not in allowed_extensions:
1777
+ raise HTTPException(
1778
+ status_code=400,
1779
+ detail=f"Unsupported file type: {suffix}. Allowed: {', '.join(allowed_extensions)}",
1780
+ )
1781
+
1782
+ try:
1783
+ import shutil
1784
+
1785
+ # Ensure watch directory exists
1786
+ _agent_instance._watch_dir.mkdir(parents=True, exist_ok=True)
1787
+
1788
+ # Add to API processing set BEFORE copying file to prevent race condition
1789
+ # with file watcher detecting the file before we add it to the set
1790
+ safe_filename = source_path.name
1791
+ with _api_processing_lock:
1792
+ _api_processing_files.add(safe_filename)
1793
+
1794
+ # Copy file to watch directory
1795
+ dest_path = _agent_instance._watch_dir / source_path.name
1796
+
1797
+ # Only copy if source is not already in watch directory
1798
+ if source_path.parent.resolve() != _agent_instance._watch_dir.resolve():
1799
+ shutil.copy2(source_path, dest_path)
1800
+ logger.info(f"File copied to watch dir: {dest_path}")
1801
+ else:
1802
+ dest_path = source_path
1803
+ logger.info(f"File already in watch dir: {dest_path}")
1804
+
1805
+ # Check if file is a duplicate before processing
1806
+ from gaia.utils import compute_file_hash
1807
+
1808
+ file_hash = compute_file_hash(str(dest_path))
1809
+ if file_hash:
1810
+ existing = _agent_instance.query(
1811
+ "SELECT id, first_name, last_name FROM patients WHERE file_hash = ?",
1812
+ (file_hash,),
1813
+ )
1814
+ if existing:
1815
+ # Clean up from set since we're returning early
1816
+ with _api_processing_lock:
1817
+ _api_processing_files.discard(safe_filename)
1818
+ patient = existing[0]
1819
+ return {
1820
+ "success": True,
1821
+ "filename": source_path.name,
1822
+ "patient_id": patient.get("id"),
1823
+ "patient_name": f"{patient.get('first_name', '')} {patient.get('last_name', '')}".strip(),
1824
+ "is_duplicate": True,
1825
+ "message": "File already processed - showing existing patient",
1826
+ }
1827
+
1828
+ def process_with_flag(fp):
1829
+ """Process file with thread-local flag to mark as API call."""
1830
+ _thread_local.is_api_call = True
1831
+ try:
1832
+ return _agent_instance._process_intake_form(fp)
1833
+ finally:
1834
+ _thread_local.is_api_call = False
1835
+
1836
+ try:
1837
+ # Process the file in a thread pool to avoid blocking the event loop
1838
+ # This allows SSE events to be sent in real-time during processing
1839
+ result = await asyncio.to_thread(process_with_flag, str(dest_path))
1840
+ finally:
1841
+ # Remove from API processing set
1842
+ with _api_processing_lock:
1843
+ _api_processing_files.discard(safe_filename)
1844
+
1845
+ if result:
1846
+ return {
1847
+ "success": True,
1848
+ "filename": source_path.name,
1849
+ "patient_id": result.get("id"),
1850
+ "patient_name": f"{result.get('first_name', '')} {result.get('last_name', '')}".strip(),
1851
+ "is_new_patient": result.get("is_new_patient", True),
1852
+ "message": "File processed successfully",
1853
+ }
1854
+ else:
1855
+ return {
1856
+ "success": False,
1857
+ "filename": source_path.name,
1858
+ "message": "Extraction failed - check if form is filled out correctly",
1859
+ }
1860
+ except HTTPException:
1861
+ # Clean up from set on error
1862
+ try:
1863
+ with _api_processing_lock:
1864
+ _api_processing_files.discard(safe_filename)
1865
+ except NameError:
1866
+ pass
1867
+ raise
1868
+ except Exception as e:
1869
+ # Clean up from set on error
1870
+ try:
1871
+ with _api_processing_lock:
1872
+ _api_processing_files.discard(safe_filename)
1873
+ except NameError:
1874
+ pass
1875
+ logger.error(f"Error processing file by path: {e}", exc_info=True)
1876
+ raise HTTPException(status_code=500, detail=str(e))
1877
+
1878
+ @app.delete("/api/database")
1879
+ async def clear_database() -> Dict[str, Any]:
1880
+ """Clear all data from the database and reset statistics."""
1881
+ if not _agent_instance:
1882
+ raise HTTPException(status_code=503, detail="Agent not initialized")
1883
+
1884
+ try:
1885
+ result = _agent_instance.clear_database()
1886
+
1887
+ if result.get("success"):
1888
+ # Clear the recent events buffer
1889
+ with _recent_events_lock:
1890
+ _recent_events.clear()
1891
+
1892
+ # Broadcast database cleared event to all SSE clients
1893
+ event = {
1894
+ "type": "database_cleared",
1895
+ "data": result.get("deleted", {}),
1896
+ "timestamp": datetime.now().isoformat(),
1897
+ }
1898
+ try:
1899
+ loop = asyncio.get_event_loop()
1900
+ if loop.is_running():
1901
+ asyncio.run_coroutine_threadsafe(broadcast_event(event), loop)
1902
+ except RuntimeError:
1903
+ pass
1904
+
1905
+ logger.info(
1906
+ f"Database cleared: {result.get('deleted', {}).get('patients', 0)} patients"
1907
+ )
1908
+ return result
1909
+ else:
1910
+ raise HTTPException(
1911
+ status_code=500,
1912
+ detail=result.get("error", "Failed to clear database"),
1913
+ )
1914
+ except HTTPException:
1915
+ raise
1916
+ except Exception as e:
1917
+ logger.error(f"Error clearing database: {e}")
1918
+ raise HTTPException(status_code=500, detail=str(e))
1919
+
1920
+ # Serve static frontend files
1921
+ dashboard_dir = Path(__file__).parent / "frontend" / "dist"
1922
+ if dashboard_dir.exists():
1923
+ app.mount(
1924
+ "/", StaticFiles(directory=str(dashboard_dir), html=True), name="static"
1925
+ )
1926
+
1927
+ @app.get("/")
1928
+ async def serve_index():
1929
+ """Serve index.html."""
1930
+ return FileResponse(dashboard_dir / "index.html")
1931
+
1932
+ else:
1933
+
1934
+ @app.get("/")
1935
+ async def no_frontend():
1936
+ """Placeholder when frontend not built."""
1937
+ return {
1938
+ "message": "EMR Dashboard API is running",
1939
+ "frontend": "not built (run npm build in dashboard/frontend)",
1940
+ "api_docs": "/docs",
1941
+ }
1942
+
1943
+ return app
1944
+
1945
+
1946
+ def run_dashboard(
1947
+ watch_dir: str = "./intake_forms",
1948
+ db_path: str = "./data/patients.db",
1949
+ host: str = "127.0.0.1",
1950
+ port: int = 8080,
1951
+ ):
1952
+ """
1953
+ Run the EMR dashboard server.
1954
+
1955
+ Args:
1956
+ watch_dir: Directory to watch for intake forms
1957
+ db_path: Path to patient database
1958
+ host: Server host (default: 127.0.0.1)
1959
+ port: Server port (default: 8080)
1960
+ """
1961
+ if not FASTAPI_AVAILABLE:
1962
+ raise ImportError(
1963
+ "FastAPI not installed. Install with: pip install 'amd-gaia[api]'"
1964
+ )
1965
+
1966
+ app = create_app(watch_dir=watch_dir, db_path=db_path)
1967
+
1968
+ uvicorn.run(
1969
+ app,
1970
+ host=host,
1971
+ port=port,
1972
+ log_level="warning", # Suppress INFO-level request logs
1973
+ access_log=False, # Disable access logs (GET/POST endpoint logs)
1974
+ )