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.
- {amd_gaia-0.14.2.dist-info → amd_gaia-0.14.3.dist-info}/METADATA +5 -2
- amd_gaia-0.14.3.dist-info/RECORD +168 -0
- {amd_gaia-0.14.2.dist-info → amd_gaia-0.14.3.dist-info}/entry_points.txt +1 -0
- gaia/__init__.py +28 -1
- gaia/agents/__init__.py +1 -1
- gaia/agents/base/__init__.py +1 -1
- gaia/agents/base/agent.py +110 -33
- gaia/agents/base/api_agent.py +1 -1
- gaia/agents/base/console.py +399 -15
- gaia/agents/base/errors.py +237 -0
- gaia/agents/base/mcp_agent.py +1 -1
- gaia/agents/base/tools.py +1 -1
- gaia/agents/blender/agent.py +1 -1
- gaia/agents/blender/agent_simple.py +1 -1
- gaia/agents/blender/app.py +1 -1
- gaia/agents/blender/app_simple.py +1 -1
- gaia/agents/blender/core/__init__.py +1 -1
- gaia/agents/blender/core/materials.py +1 -1
- gaia/agents/blender/core/objects.py +1 -1
- gaia/agents/blender/core/rendering.py +1 -1
- gaia/agents/blender/core/scene.py +1 -1
- gaia/agents/blender/core/view.py +1 -1
- gaia/agents/chat/__init__.py +1 -1
- gaia/agents/chat/agent.py +36 -153
- gaia/agents/chat/app.py +1 -1
- gaia/agents/chat/session.py +1 -1
- gaia/agents/chat/tools/__init__.py +1 -1
- gaia/agents/chat/tools/file_tools.py +1 -1
- gaia/agents/chat/tools/rag_tools.py +1 -1
- gaia/agents/chat/tools/shell_tools.py +1 -1
- gaia/agents/code/__init__.py +1 -1
- gaia/agents/code/agent.py +3 -1
- gaia/agents/code/orchestration/__init__.py +1 -1
- gaia/agents/code/orchestration/checklist_executor.py +1 -1
- gaia/agents/code/orchestration/checklist_generator.py +1 -1
- gaia/agents/code/orchestration/factories/__init__.py +1 -1
- gaia/agents/code/orchestration/factories/base.py +1 -1
- gaia/agents/code/orchestration/factories/nextjs_factory.py +1 -1
- gaia/agents/code/orchestration/factories/python_factory.py +1 -1
- gaia/agents/code/orchestration/orchestrator.py +212 -1
- gaia/agents/code/orchestration/project_analyzer.py +1 -1
- gaia/agents/code/orchestration/steps/__init__.py +1 -1
- gaia/agents/code/orchestration/steps/base.py +1 -1
- gaia/agents/code/orchestration/steps/error_handler.py +1 -1
- gaia/agents/code/orchestration/steps/nextjs.py +1 -1
- gaia/agents/code/orchestration/steps/python.py +1 -1
- gaia/agents/code/orchestration/template_catalog.py +1 -1
- gaia/agents/code/orchestration/workflows/__init__.py +1 -1
- gaia/agents/code/orchestration/workflows/base.py +1 -1
- gaia/agents/code/orchestration/workflows/nextjs.py +1 -1
- gaia/agents/code/orchestration/workflows/python.py +1 -1
- gaia/agents/code/prompts/__init__.py +1 -1
- gaia/agents/code/prompts/base_prompt.py +1 -1
- gaia/agents/code/prompts/code_patterns.py +1 -1
- gaia/agents/code/prompts/nextjs_prompt.py +1 -1
- gaia/agents/code/prompts/python_prompt.py +1 -1
- gaia/agents/code/schema_inference.py +1 -1
- gaia/agents/code/system_prompt.py +1 -1
- gaia/agents/code/tools/__init__.py +1 -1
- gaia/agents/code/tools/cli_tools.py +1 -1
- gaia/agents/code/tools/code_formatting.py +1 -1
- gaia/agents/code/tools/code_tools.py +1 -1
- gaia/agents/code/tools/error_fixing.py +1 -1
- gaia/agents/code/tools/external_tools.py +1 -1
- gaia/agents/code/tools/prisma_tools.py +1 -1
- gaia/agents/code/tools/project_management.py +1 -1
- gaia/agents/code/tools/testing.py +1 -1
- gaia/agents/code/tools/typescript_tools.py +1 -1
- gaia/agents/code/tools/validation_parsing.py +1 -1
- gaia/agents/code/tools/validation_tools.py +5 -2
- gaia/agents/code/tools/web_dev_tools.py +1 -2
- gaia/agents/docker/__init__.py +1 -1
- gaia/agents/emr/__init__.py +8 -0
- gaia/agents/emr/agent.py +1506 -0
- gaia/agents/emr/cli.py +1322 -0
- gaia/agents/emr/constants.py +475 -0
- gaia/agents/emr/dashboard/__init__.py +4 -0
- gaia/agents/emr/dashboard/server.py +1974 -0
- gaia/agents/routing/__init__.py +1 -1
- gaia/agents/routing/agent.py +65 -7
- gaia/agents/routing/system_prompt.py +1 -1
- gaia/api/__init__.py +1 -1
- gaia/api/agent_registry.py +1 -1
- gaia/api/app.py +1 -1
- gaia/api/openai_server.py +1 -1
- gaia/api/schemas.py +1 -1
- gaia/api/sse_handler.py +5 -2
- gaia/apps/__init__.py +1 -1
- gaia/apps/llm/__init__.py +1 -1
- gaia/audio/__init__.py +1 -1
- gaia/audio/audio_client.py +1 -1
- gaia/audio/audio_recorder.py +1 -1
- gaia/audio/kokoro_tts.py +1 -1
- gaia/audio/whisper_asr.py +1 -1
- gaia/chat/__init__.py +1 -1
- gaia/chat/prompts.py +1 -1
- gaia/chat/sdk.py +25 -0
- gaia/cli.py +2 -2
- gaia/database/__init__.py +10 -0
- gaia/database/agent.py +176 -0
- gaia/database/mixin.py +290 -0
- gaia/database/testing.py +64 -0
- gaia/eval/batch_experiment.py +1 -1
- gaia/eval/claude.py +1 -1
- gaia/eval/config.py +1 -1
- gaia/eval/email_generator.py +1 -1
- gaia/eval/eval.py +1 -1
- gaia/eval/groundtruth.py +1 -1
- gaia/eval/transcript_generator.py +1 -1
- gaia/eval/webapp/public/app.js +1 -1
- gaia/eval/webapp/server.js +1 -1
- gaia/eval/webapp/test-setup.js +1 -1
- gaia/llm/__init__.py +1 -1
- gaia/llm/lemonade_client.py +149 -11
- gaia/llm/lemonade_manager.py +36 -11
- gaia/llm/llm_client.py +1 -1
- gaia/llm/vlm_client.py +93 -18
- gaia/logger.py +1 -1
- gaia/mcp/agent_mcp_server.py +1 -1
- gaia/mcp/blender_mcp_client.py +1 -1
- gaia/mcp/blender_mcp_server.py +1 -1
- gaia/mcp/context7_cache.py +1 -1
- gaia/mcp/servers/__init__.py +1 -1
- gaia/mcp/servers/docker_mcp.py +1 -1
- gaia/security.py +1 -1
- gaia/testing/__init__.py +87 -0
- gaia/testing/assertions.py +330 -0
- gaia/testing/fixtures.py +333 -0
- gaia/testing/mocks.py +493 -0
- gaia/util.py +1 -1
- gaia/utils/__init__.py +33 -0
- gaia/utils/file_watcher.py +675 -0
- gaia/utils/parsing.py +223 -0
- gaia/version.py +2 -2
- amd_gaia-0.14.2.dist-info/RECORD +0 -800
- gaia/eval/webapp/node_modules/.bin/mime +0 -16
- gaia/eval/webapp/node_modules/.bin/mime.cmd +0 -17
- gaia/eval/webapp/node_modules/.bin/mime.ps1 +0 -28
- gaia/eval/webapp/node_modules/.package-lock.json +0 -865
- gaia/eval/webapp/node_modules/accepts/HISTORY.md +0 -243
- gaia/eval/webapp/node_modules/accepts/LICENSE +0 -23
- gaia/eval/webapp/node_modules/accepts/README.md +0 -140
- gaia/eval/webapp/node_modules/accepts/index.js +0 -238
- gaia/eval/webapp/node_modules/accepts/package.json +0 -47
- gaia/eval/webapp/node_modules/array-flatten/LICENSE +0 -21
- gaia/eval/webapp/node_modules/array-flatten/README.md +0 -43
- gaia/eval/webapp/node_modules/array-flatten/array-flatten.js +0 -64
- gaia/eval/webapp/node_modules/array-flatten/package.json +0 -39
- gaia/eval/webapp/node_modules/body-parser/HISTORY.md +0 -672
- gaia/eval/webapp/node_modules/body-parser/LICENSE +0 -23
- gaia/eval/webapp/node_modules/body-parser/README.md +0 -476
- gaia/eval/webapp/node_modules/body-parser/SECURITY.md +0 -25
- gaia/eval/webapp/node_modules/body-parser/index.js +0 -156
- gaia/eval/webapp/node_modules/body-parser/lib/read.js +0 -205
- gaia/eval/webapp/node_modules/body-parser/lib/types/json.js +0 -247
- gaia/eval/webapp/node_modules/body-parser/lib/types/raw.js +0 -101
- gaia/eval/webapp/node_modules/body-parser/lib/types/text.js +0 -121
- gaia/eval/webapp/node_modules/body-parser/lib/types/urlencoded.js +0 -307
- gaia/eval/webapp/node_modules/body-parser/package.json +0 -56
- gaia/eval/webapp/node_modules/bytes/History.md +0 -97
- gaia/eval/webapp/node_modules/bytes/LICENSE +0 -23
- gaia/eval/webapp/node_modules/bytes/Readme.md +0 -152
- gaia/eval/webapp/node_modules/bytes/index.js +0 -170
- gaia/eval/webapp/node_modules/bytes/package.json +0 -42
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/.eslintrc +0 -17
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/.github/FUNDING.yml +0 -12
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/.nycrc +0 -9
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/CHANGELOG.md +0 -30
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/LICENSE +0 -21
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/README.md +0 -62
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/actualApply.d.ts +0 -1
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/actualApply.js +0 -10
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/applyBind.d.ts +0 -19
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/applyBind.js +0 -10
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/functionApply.d.ts +0 -1
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/functionApply.js +0 -4
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/functionCall.d.ts +0 -1
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/functionCall.js +0 -4
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/index.d.ts +0 -64
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/index.js +0 -15
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/package.json +0 -85
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/reflectApply.d.ts +0 -3
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/reflectApply.js +0 -4
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/test/index.js +0 -63
- gaia/eval/webapp/node_modules/call-bind-apply-helpers/tsconfig.json +0 -9
- gaia/eval/webapp/node_modules/call-bound/.eslintrc +0 -13
- gaia/eval/webapp/node_modules/call-bound/.github/FUNDING.yml +0 -12
- gaia/eval/webapp/node_modules/call-bound/.nycrc +0 -9
- gaia/eval/webapp/node_modules/call-bound/CHANGELOG.md +0 -42
- gaia/eval/webapp/node_modules/call-bound/LICENSE +0 -21
- gaia/eval/webapp/node_modules/call-bound/README.md +0 -53
- gaia/eval/webapp/node_modules/call-bound/index.d.ts +0 -94
- gaia/eval/webapp/node_modules/call-bound/index.js +0 -19
- gaia/eval/webapp/node_modules/call-bound/package.json +0 -99
- gaia/eval/webapp/node_modules/call-bound/test/index.js +0 -61
- gaia/eval/webapp/node_modules/call-bound/tsconfig.json +0 -10
- gaia/eval/webapp/node_modules/content-disposition/HISTORY.md +0 -60
- gaia/eval/webapp/node_modules/content-disposition/LICENSE +0 -22
- gaia/eval/webapp/node_modules/content-disposition/README.md +0 -142
- gaia/eval/webapp/node_modules/content-disposition/index.js +0 -458
- gaia/eval/webapp/node_modules/content-disposition/package.json +0 -44
- gaia/eval/webapp/node_modules/content-type/HISTORY.md +0 -29
- gaia/eval/webapp/node_modules/content-type/LICENSE +0 -22
- gaia/eval/webapp/node_modules/content-type/README.md +0 -94
- gaia/eval/webapp/node_modules/content-type/index.js +0 -225
- gaia/eval/webapp/node_modules/content-type/package.json +0 -42
- gaia/eval/webapp/node_modules/cookie/LICENSE +0 -24
- gaia/eval/webapp/node_modules/cookie/README.md +0 -317
- gaia/eval/webapp/node_modules/cookie/SECURITY.md +0 -25
- gaia/eval/webapp/node_modules/cookie/index.js +0 -334
- gaia/eval/webapp/node_modules/cookie/package.json +0 -44
- gaia/eval/webapp/node_modules/cookie-signature/.npmignore +0 -4
- gaia/eval/webapp/node_modules/cookie-signature/History.md +0 -38
- gaia/eval/webapp/node_modules/cookie-signature/Readme.md +0 -42
- gaia/eval/webapp/node_modules/cookie-signature/index.js +0 -51
- gaia/eval/webapp/node_modules/cookie-signature/package.json +0 -18
- gaia/eval/webapp/node_modules/debug/.coveralls.yml +0 -1
- gaia/eval/webapp/node_modules/debug/.eslintrc +0 -11
- gaia/eval/webapp/node_modules/debug/.npmignore +0 -9
- gaia/eval/webapp/node_modules/debug/.travis.yml +0 -14
- gaia/eval/webapp/node_modules/debug/CHANGELOG.md +0 -362
- gaia/eval/webapp/node_modules/debug/LICENSE +0 -19
- gaia/eval/webapp/node_modules/debug/Makefile +0 -50
- gaia/eval/webapp/node_modules/debug/README.md +0 -312
- gaia/eval/webapp/node_modules/debug/component.json +0 -19
- gaia/eval/webapp/node_modules/debug/karma.conf.js +0 -70
- gaia/eval/webapp/node_modules/debug/node.js +0 -1
- gaia/eval/webapp/node_modules/debug/package.json +0 -49
- gaia/eval/webapp/node_modules/debug/src/browser.js +0 -185
- gaia/eval/webapp/node_modules/debug/src/debug.js +0 -202
- gaia/eval/webapp/node_modules/debug/src/index.js +0 -10
- gaia/eval/webapp/node_modules/debug/src/inspector-log.js +0 -15
- gaia/eval/webapp/node_modules/debug/src/node.js +0 -248
- gaia/eval/webapp/node_modules/depd/History.md +0 -103
- gaia/eval/webapp/node_modules/depd/LICENSE +0 -22
- gaia/eval/webapp/node_modules/depd/Readme.md +0 -280
- gaia/eval/webapp/node_modules/depd/index.js +0 -538
- gaia/eval/webapp/node_modules/depd/lib/browser/index.js +0 -77
- gaia/eval/webapp/node_modules/depd/package.json +0 -45
- gaia/eval/webapp/node_modules/destroy/LICENSE +0 -23
- gaia/eval/webapp/node_modules/destroy/README.md +0 -63
- gaia/eval/webapp/node_modules/destroy/index.js +0 -209
- gaia/eval/webapp/node_modules/destroy/package.json +0 -48
- gaia/eval/webapp/node_modules/dunder-proto/.eslintrc +0 -5
- gaia/eval/webapp/node_modules/dunder-proto/.github/FUNDING.yml +0 -12
- gaia/eval/webapp/node_modules/dunder-proto/.nycrc +0 -13
- gaia/eval/webapp/node_modules/dunder-proto/CHANGELOG.md +0 -24
- gaia/eval/webapp/node_modules/dunder-proto/LICENSE +0 -21
- gaia/eval/webapp/node_modules/dunder-proto/README.md +0 -54
- gaia/eval/webapp/node_modules/dunder-proto/get.d.ts +0 -5
- gaia/eval/webapp/node_modules/dunder-proto/get.js +0 -30
- gaia/eval/webapp/node_modules/dunder-proto/package.json +0 -76
- gaia/eval/webapp/node_modules/dunder-proto/set.d.ts +0 -5
- gaia/eval/webapp/node_modules/dunder-proto/set.js +0 -35
- gaia/eval/webapp/node_modules/dunder-proto/test/get.js +0 -34
- gaia/eval/webapp/node_modules/dunder-proto/test/index.js +0 -4
- gaia/eval/webapp/node_modules/dunder-proto/test/set.js +0 -50
- gaia/eval/webapp/node_modules/dunder-proto/tsconfig.json +0 -9
- gaia/eval/webapp/node_modules/ee-first/LICENSE +0 -22
- gaia/eval/webapp/node_modules/ee-first/README.md +0 -80
- gaia/eval/webapp/node_modules/ee-first/index.js +0 -95
- gaia/eval/webapp/node_modules/ee-first/package.json +0 -29
- gaia/eval/webapp/node_modules/encodeurl/LICENSE +0 -22
- gaia/eval/webapp/node_modules/encodeurl/README.md +0 -109
- gaia/eval/webapp/node_modules/encodeurl/index.js +0 -60
- gaia/eval/webapp/node_modules/encodeurl/package.json +0 -40
- gaia/eval/webapp/node_modules/es-define-property/.eslintrc +0 -13
- gaia/eval/webapp/node_modules/es-define-property/.github/FUNDING.yml +0 -12
- gaia/eval/webapp/node_modules/es-define-property/.nycrc +0 -9
- gaia/eval/webapp/node_modules/es-define-property/CHANGELOG.md +0 -29
- gaia/eval/webapp/node_modules/es-define-property/LICENSE +0 -21
- gaia/eval/webapp/node_modules/es-define-property/README.md +0 -49
- gaia/eval/webapp/node_modules/es-define-property/index.d.ts +0 -3
- gaia/eval/webapp/node_modules/es-define-property/index.js +0 -14
- gaia/eval/webapp/node_modules/es-define-property/package.json +0 -81
- gaia/eval/webapp/node_modules/es-define-property/test/index.js +0 -56
- gaia/eval/webapp/node_modules/es-define-property/tsconfig.json +0 -10
- gaia/eval/webapp/node_modules/es-errors/.eslintrc +0 -5
- gaia/eval/webapp/node_modules/es-errors/.github/FUNDING.yml +0 -12
- gaia/eval/webapp/node_modules/es-errors/CHANGELOG.md +0 -40
- gaia/eval/webapp/node_modules/es-errors/LICENSE +0 -21
- gaia/eval/webapp/node_modules/es-errors/README.md +0 -55
- gaia/eval/webapp/node_modules/es-errors/eval.d.ts +0 -3
- gaia/eval/webapp/node_modules/es-errors/eval.js +0 -4
- gaia/eval/webapp/node_modules/es-errors/index.d.ts +0 -3
- gaia/eval/webapp/node_modules/es-errors/index.js +0 -4
- gaia/eval/webapp/node_modules/es-errors/package.json +0 -80
- gaia/eval/webapp/node_modules/es-errors/range.d.ts +0 -3
- gaia/eval/webapp/node_modules/es-errors/range.js +0 -4
- gaia/eval/webapp/node_modules/es-errors/ref.d.ts +0 -3
- gaia/eval/webapp/node_modules/es-errors/ref.js +0 -4
- gaia/eval/webapp/node_modules/es-errors/syntax.d.ts +0 -3
- gaia/eval/webapp/node_modules/es-errors/syntax.js +0 -4
- gaia/eval/webapp/node_modules/es-errors/test/index.js +0 -19
- gaia/eval/webapp/node_modules/es-errors/tsconfig.json +0 -49
- gaia/eval/webapp/node_modules/es-errors/type.d.ts +0 -3
- gaia/eval/webapp/node_modules/es-errors/type.js +0 -4
- gaia/eval/webapp/node_modules/es-errors/uri.d.ts +0 -3
- gaia/eval/webapp/node_modules/es-errors/uri.js +0 -4
- gaia/eval/webapp/node_modules/es-object-atoms/.eslintrc +0 -16
- gaia/eval/webapp/node_modules/es-object-atoms/.github/FUNDING.yml +0 -12
- gaia/eval/webapp/node_modules/es-object-atoms/CHANGELOG.md +0 -37
- gaia/eval/webapp/node_modules/es-object-atoms/LICENSE +0 -21
- gaia/eval/webapp/node_modules/es-object-atoms/README.md +0 -63
- gaia/eval/webapp/node_modules/es-object-atoms/RequireObjectCoercible.d.ts +0 -3
- gaia/eval/webapp/node_modules/es-object-atoms/RequireObjectCoercible.js +0 -11
- gaia/eval/webapp/node_modules/es-object-atoms/ToObject.d.ts +0 -7
- gaia/eval/webapp/node_modules/es-object-atoms/ToObject.js +0 -10
- gaia/eval/webapp/node_modules/es-object-atoms/index.d.ts +0 -3
- gaia/eval/webapp/node_modules/es-object-atoms/index.js +0 -4
- gaia/eval/webapp/node_modules/es-object-atoms/isObject.d.ts +0 -3
- gaia/eval/webapp/node_modules/es-object-atoms/isObject.js +0 -6
- gaia/eval/webapp/node_modules/es-object-atoms/package.json +0 -80
- gaia/eval/webapp/node_modules/es-object-atoms/test/index.js +0 -38
- gaia/eval/webapp/node_modules/es-object-atoms/tsconfig.json +0 -6
- gaia/eval/webapp/node_modules/escape-html/LICENSE +0 -24
- gaia/eval/webapp/node_modules/escape-html/Readme.md +0 -43
- gaia/eval/webapp/node_modules/escape-html/index.js +0 -78
- gaia/eval/webapp/node_modules/escape-html/package.json +0 -24
- gaia/eval/webapp/node_modules/etag/HISTORY.md +0 -83
- gaia/eval/webapp/node_modules/etag/LICENSE +0 -22
- gaia/eval/webapp/node_modules/etag/README.md +0 -159
- gaia/eval/webapp/node_modules/etag/index.js +0 -131
- gaia/eval/webapp/node_modules/etag/package.json +0 -47
- gaia/eval/webapp/node_modules/express/History.md +0 -3656
- gaia/eval/webapp/node_modules/express/LICENSE +0 -24
- gaia/eval/webapp/node_modules/express/Readme.md +0 -260
- gaia/eval/webapp/node_modules/express/index.js +0 -11
- gaia/eval/webapp/node_modules/express/lib/application.js +0 -661
- gaia/eval/webapp/node_modules/express/lib/express.js +0 -116
- gaia/eval/webapp/node_modules/express/lib/middleware/init.js +0 -43
- gaia/eval/webapp/node_modules/express/lib/middleware/query.js +0 -47
- gaia/eval/webapp/node_modules/express/lib/request.js +0 -525
- gaia/eval/webapp/node_modules/express/lib/response.js +0 -1179
- gaia/eval/webapp/node_modules/express/lib/router/index.js +0 -673
- gaia/eval/webapp/node_modules/express/lib/router/layer.js +0 -181
- gaia/eval/webapp/node_modules/express/lib/router/route.js +0 -230
- gaia/eval/webapp/node_modules/express/lib/utils.js +0 -303
- gaia/eval/webapp/node_modules/express/lib/view.js +0 -182
- gaia/eval/webapp/node_modules/express/package.json +0 -102
- gaia/eval/webapp/node_modules/finalhandler/HISTORY.md +0 -210
- gaia/eval/webapp/node_modules/finalhandler/LICENSE +0 -22
- gaia/eval/webapp/node_modules/finalhandler/README.md +0 -147
- gaia/eval/webapp/node_modules/finalhandler/SECURITY.md +0 -25
- gaia/eval/webapp/node_modules/finalhandler/index.js +0 -341
- gaia/eval/webapp/node_modules/finalhandler/package.json +0 -47
- gaia/eval/webapp/node_modules/forwarded/HISTORY.md +0 -21
- gaia/eval/webapp/node_modules/forwarded/LICENSE +0 -22
- gaia/eval/webapp/node_modules/forwarded/README.md +0 -57
- gaia/eval/webapp/node_modules/forwarded/index.js +0 -90
- gaia/eval/webapp/node_modules/forwarded/package.json +0 -45
- gaia/eval/webapp/node_modules/fresh/HISTORY.md +0 -70
- gaia/eval/webapp/node_modules/fresh/LICENSE +0 -23
- gaia/eval/webapp/node_modules/fresh/README.md +0 -119
- gaia/eval/webapp/node_modules/fresh/index.js +0 -137
- gaia/eval/webapp/node_modules/fresh/package.json +0 -46
- gaia/eval/webapp/node_modules/fs/README.md +0 -9
- gaia/eval/webapp/node_modules/fs/package.json +0 -20
- gaia/eval/webapp/node_modules/function-bind/.eslintrc +0 -21
- gaia/eval/webapp/node_modules/function-bind/.github/FUNDING.yml +0 -12
- gaia/eval/webapp/node_modules/function-bind/.github/SECURITY.md +0 -3
- gaia/eval/webapp/node_modules/function-bind/.nycrc +0 -13
- gaia/eval/webapp/node_modules/function-bind/CHANGELOG.md +0 -136
- gaia/eval/webapp/node_modules/function-bind/LICENSE +0 -20
- gaia/eval/webapp/node_modules/function-bind/README.md +0 -46
- gaia/eval/webapp/node_modules/function-bind/implementation.js +0 -84
- gaia/eval/webapp/node_modules/function-bind/index.js +0 -5
- gaia/eval/webapp/node_modules/function-bind/package.json +0 -87
- gaia/eval/webapp/node_modules/function-bind/test/.eslintrc +0 -9
- gaia/eval/webapp/node_modules/function-bind/test/index.js +0 -252
- gaia/eval/webapp/node_modules/get-intrinsic/.eslintrc +0 -42
- gaia/eval/webapp/node_modules/get-intrinsic/.github/FUNDING.yml +0 -12
- gaia/eval/webapp/node_modules/get-intrinsic/.nycrc +0 -9
- gaia/eval/webapp/node_modules/get-intrinsic/CHANGELOG.md +0 -186
- gaia/eval/webapp/node_modules/get-intrinsic/LICENSE +0 -21
- gaia/eval/webapp/node_modules/get-intrinsic/README.md +0 -71
- gaia/eval/webapp/node_modules/get-intrinsic/index.js +0 -378
- gaia/eval/webapp/node_modules/get-intrinsic/package.json +0 -97
- gaia/eval/webapp/node_modules/get-intrinsic/test/GetIntrinsic.js +0 -274
- gaia/eval/webapp/node_modules/get-proto/.eslintrc +0 -10
- gaia/eval/webapp/node_modules/get-proto/.github/FUNDING.yml +0 -12
- gaia/eval/webapp/node_modules/get-proto/.nycrc +0 -9
- gaia/eval/webapp/node_modules/get-proto/CHANGELOG.md +0 -21
- gaia/eval/webapp/node_modules/get-proto/LICENSE +0 -21
- gaia/eval/webapp/node_modules/get-proto/Object.getPrototypeOf.d.ts +0 -5
- gaia/eval/webapp/node_modules/get-proto/Object.getPrototypeOf.js +0 -6
- gaia/eval/webapp/node_modules/get-proto/README.md +0 -50
- gaia/eval/webapp/node_modules/get-proto/Reflect.getPrototypeOf.d.ts +0 -3
- gaia/eval/webapp/node_modules/get-proto/Reflect.getPrototypeOf.js +0 -4
- gaia/eval/webapp/node_modules/get-proto/index.d.ts +0 -5
- gaia/eval/webapp/node_modules/get-proto/index.js +0 -27
- gaia/eval/webapp/node_modules/get-proto/package.json +0 -81
- gaia/eval/webapp/node_modules/get-proto/test/index.js +0 -68
- gaia/eval/webapp/node_modules/get-proto/tsconfig.json +0 -9
- gaia/eval/webapp/node_modules/gopd/.eslintrc +0 -16
- gaia/eval/webapp/node_modules/gopd/.github/FUNDING.yml +0 -12
- gaia/eval/webapp/node_modules/gopd/CHANGELOG.md +0 -45
- gaia/eval/webapp/node_modules/gopd/LICENSE +0 -21
- gaia/eval/webapp/node_modules/gopd/README.md +0 -40
- gaia/eval/webapp/node_modules/gopd/gOPD.d.ts +0 -1
- gaia/eval/webapp/node_modules/gopd/gOPD.js +0 -4
- gaia/eval/webapp/node_modules/gopd/index.d.ts +0 -5
- gaia/eval/webapp/node_modules/gopd/index.js +0 -15
- gaia/eval/webapp/node_modules/gopd/package.json +0 -77
- gaia/eval/webapp/node_modules/gopd/test/index.js +0 -36
- gaia/eval/webapp/node_modules/gopd/tsconfig.json +0 -9
- gaia/eval/webapp/node_modules/has-symbols/.eslintrc +0 -11
- gaia/eval/webapp/node_modules/has-symbols/.github/FUNDING.yml +0 -12
- gaia/eval/webapp/node_modules/has-symbols/.nycrc +0 -9
- gaia/eval/webapp/node_modules/has-symbols/CHANGELOG.md +0 -91
- gaia/eval/webapp/node_modules/has-symbols/LICENSE +0 -21
- gaia/eval/webapp/node_modules/has-symbols/README.md +0 -46
- gaia/eval/webapp/node_modules/has-symbols/index.d.ts +0 -3
- gaia/eval/webapp/node_modules/has-symbols/index.js +0 -14
- gaia/eval/webapp/node_modules/has-symbols/package.json +0 -111
- gaia/eval/webapp/node_modules/has-symbols/shams.d.ts +0 -3
- gaia/eval/webapp/node_modules/has-symbols/shams.js +0 -45
- gaia/eval/webapp/node_modules/has-symbols/test/index.js +0 -22
- gaia/eval/webapp/node_modules/has-symbols/test/shams/core-js.js +0 -29
- gaia/eval/webapp/node_modules/has-symbols/test/shams/get-own-property-symbols.js +0 -29
- gaia/eval/webapp/node_modules/has-symbols/test/tests.js +0 -58
- gaia/eval/webapp/node_modules/has-symbols/tsconfig.json +0 -10
- gaia/eval/webapp/node_modules/hasown/.eslintrc +0 -5
- gaia/eval/webapp/node_modules/hasown/.github/FUNDING.yml +0 -12
- gaia/eval/webapp/node_modules/hasown/.nycrc +0 -13
- gaia/eval/webapp/node_modules/hasown/CHANGELOG.md +0 -40
- gaia/eval/webapp/node_modules/hasown/LICENSE +0 -21
- gaia/eval/webapp/node_modules/hasown/README.md +0 -40
- gaia/eval/webapp/node_modules/hasown/index.d.ts +0 -3
- gaia/eval/webapp/node_modules/hasown/index.js +0 -8
- gaia/eval/webapp/node_modules/hasown/package.json +0 -92
- gaia/eval/webapp/node_modules/hasown/tsconfig.json +0 -6
- gaia/eval/webapp/node_modules/http-errors/HISTORY.md +0 -180
- gaia/eval/webapp/node_modules/http-errors/LICENSE +0 -23
- gaia/eval/webapp/node_modules/http-errors/README.md +0 -169
- gaia/eval/webapp/node_modules/http-errors/index.js +0 -289
- gaia/eval/webapp/node_modules/http-errors/package.json +0 -50
- gaia/eval/webapp/node_modules/iconv-lite/Changelog.md +0 -162
- gaia/eval/webapp/node_modules/iconv-lite/LICENSE +0 -21
- gaia/eval/webapp/node_modules/iconv-lite/README.md +0 -156
- gaia/eval/webapp/node_modules/iconv-lite/encodings/dbcs-codec.js +0 -555
- gaia/eval/webapp/node_modules/iconv-lite/encodings/dbcs-data.js +0 -176
- gaia/eval/webapp/node_modules/iconv-lite/encodings/index.js +0 -22
- gaia/eval/webapp/node_modules/iconv-lite/encodings/internal.js +0 -188
- gaia/eval/webapp/node_modules/iconv-lite/encodings/sbcs-codec.js +0 -72
- gaia/eval/webapp/node_modules/iconv-lite/encodings/sbcs-data-generated.js +0 -451
- gaia/eval/webapp/node_modules/iconv-lite/encodings/sbcs-data.js +0 -174
- gaia/eval/webapp/node_modules/iconv-lite/encodings/tables/big5-added.json +0 -122
- gaia/eval/webapp/node_modules/iconv-lite/encodings/tables/cp936.json +0 -264
- gaia/eval/webapp/node_modules/iconv-lite/encodings/tables/cp949.json +0 -273
- gaia/eval/webapp/node_modules/iconv-lite/encodings/tables/cp950.json +0 -177
- gaia/eval/webapp/node_modules/iconv-lite/encodings/tables/eucjp.json +0 -182
- gaia/eval/webapp/node_modules/iconv-lite/encodings/tables/gb18030-ranges.json +0 -1
- gaia/eval/webapp/node_modules/iconv-lite/encodings/tables/gbk-added.json +0 -55
- gaia/eval/webapp/node_modules/iconv-lite/encodings/tables/shiftjis.json +0 -125
- gaia/eval/webapp/node_modules/iconv-lite/encodings/utf16.js +0 -177
- gaia/eval/webapp/node_modules/iconv-lite/encodings/utf7.js +0 -290
- gaia/eval/webapp/node_modules/iconv-lite/lib/bom-handling.js +0 -52
- gaia/eval/webapp/node_modules/iconv-lite/lib/extend-node.js +0 -217
- gaia/eval/webapp/node_modules/iconv-lite/lib/index.d.ts +0 -24
- gaia/eval/webapp/node_modules/iconv-lite/lib/index.js +0 -153
- gaia/eval/webapp/node_modules/iconv-lite/lib/streams.js +0 -121
- gaia/eval/webapp/node_modules/iconv-lite/package.json +0 -46
- gaia/eval/webapp/node_modules/inherits/LICENSE +0 -16
- gaia/eval/webapp/node_modules/inherits/README.md +0 -42
- gaia/eval/webapp/node_modules/inherits/inherits.js +0 -9
- gaia/eval/webapp/node_modules/inherits/inherits_browser.js +0 -27
- gaia/eval/webapp/node_modules/inherits/package.json +0 -29
- gaia/eval/webapp/node_modules/ipaddr.js/LICENSE +0 -19
- gaia/eval/webapp/node_modules/ipaddr.js/README.md +0 -233
- gaia/eval/webapp/node_modules/ipaddr.js/ipaddr.min.js +0 -1
- gaia/eval/webapp/node_modules/ipaddr.js/lib/ipaddr.js +0 -673
- gaia/eval/webapp/node_modules/ipaddr.js/lib/ipaddr.js.d.ts +0 -68
- gaia/eval/webapp/node_modules/ipaddr.js/package.json +0 -35
- gaia/eval/webapp/node_modules/math-intrinsics/.eslintrc +0 -16
- gaia/eval/webapp/node_modules/math-intrinsics/.github/FUNDING.yml +0 -12
- gaia/eval/webapp/node_modules/math-intrinsics/CHANGELOG.md +0 -24
- gaia/eval/webapp/node_modules/math-intrinsics/LICENSE +0 -21
- gaia/eval/webapp/node_modules/math-intrinsics/README.md +0 -50
- gaia/eval/webapp/node_modules/math-intrinsics/abs.d.ts +0 -1
- gaia/eval/webapp/node_modules/math-intrinsics/abs.js +0 -4
- gaia/eval/webapp/node_modules/math-intrinsics/constants/maxArrayLength.d.ts +0 -3
- gaia/eval/webapp/node_modules/math-intrinsics/constants/maxArrayLength.js +0 -4
- gaia/eval/webapp/node_modules/math-intrinsics/constants/maxSafeInteger.d.ts +0 -3
- gaia/eval/webapp/node_modules/math-intrinsics/constants/maxSafeInteger.js +0 -5
- gaia/eval/webapp/node_modules/math-intrinsics/constants/maxValue.d.ts +0 -3
- gaia/eval/webapp/node_modules/math-intrinsics/constants/maxValue.js +0 -5
- gaia/eval/webapp/node_modules/math-intrinsics/floor.d.ts +0 -1
- gaia/eval/webapp/node_modules/math-intrinsics/floor.js +0 -4
- gaia/eval/webapp/node_modules/math-intrinsics/isFinite.d.ts +0 -3
- gaia/eval/webapp/node_modules/math-intrinsics/isFinite.js +0 -12
- gaia/eval/webapp/node_modules/math-intrinsics/isInteger.d.ts +0 -3
- gaia/eval/webapp/node_modules/math-intrinsics/isInteger.js +0 -16
- gaia/eval/webapp/node_modules/math-intrinsics/isNaN.d.ts +0 -1
- gaia/eval/webapp/node_modules/math-intrinsics/isNaN.js +0 -6
- gaia/eval/webapp/node_modules/math-intrinsics/isNegativeZero.d.ts +0 -3
- gaia/eval/webapp/node_modules/math-intrinsics/isNegativeZero.js +0 -6
- gaia/eval/webapp/node_modules/math-intrinsics/max.d.ts +0 -1
- gaia/eval/webapp/node_modules/math-intrinsics/max.js +0 -4
- gaia/eval/webapp/node_modules/math-intrinsics/min.d.ts +0 -1
- gaia/eval/webapp/node_modules/math-intrinsics/min.js +0 -4
- gaia/eval/webapp/node_modules/math-intrinsics/mod.d.ts +0 -3
- gaia/eval/webapp/node_modules/math-intrinsics/mod.js +0 -9
- gaia/eval/webapp/node_modules/math-intrinsics/package.json +0 -86
- gaia/eval/webapp/node_modules/math-intrinsics/pow.d.ts +0 -1
- gaia/eval/webapp/node_modules/math-intrinsics/pow.js +0 -4
- gaia/eval/webapp/node_modules/math-intrinsics/round.d.ts +0 -1
- gaia/eval/webapp/node_modules/math-intrinsics/round.js +0 -4
- gaia/eval/webapp/node_modules/math-intrinsics/sign.d.ts +0 -3
- gaia/eval/webapp/node_modules/math-intrinsics/sign.js +0 -11
- gaia/eval/webapp/node_modules/math-intrinsics/test/index.js +0 -192
- gaia/eval/webapp/node_modules/math-intrinsics/tsconfig.json +0 -3
- gaia/eval/webapp/node_modules/media-typer/HISTORY.md +0 -22
- gaia/eval/webapp/node_modules/media-typer/LICENSE +0 -22
- gaia/eval/webapp/node_modules/media-typer/README.md +0 -81
- gaia/eval/webapp/node_modules/media-typer/index.js +0 -270
- gaia/eval/webapp/node_modules/media-typer/package.json +0 -26
- gaia/eval/webapp/node_modules/merge-descriptors/HISTORY.md +0 -21
- gaia/eval/webapp/node_modules/merge-descriptors/LICENSE +0 -23
- gaia/eval/webapp/node_modules/merge-descriptors/README.md +0 -49
- gaia/eval/webapp/node_modules/merge-descriptors/index.js +0 -60
- gaia/eval/webapp/node_modules/merge-descriptors/package.json +0 -39
- gaia/eval/webapp/node_modules/methods/HISTORY.md +0 -29
- gaia/eval/webapp/node_modules/methods/LICENSE +0 -24
- gaia/eval/webapp/node_modules/methods/README.md +0 -51
- gaia/eval/webapp/node_modules/methods/index.js +0 -69
- gaia/eval/webapp/node_modules/methods/package.json +0 -36
- gaia/eval/webapp/node_modules/mime/.npmignore +0 -0
- gaia/eval/webapp/node_modules/mime/CHANGELOG.md +0 -164
- gaia/eval/webapp/node_modules/mime/LICENSE +0 -21
- gaia/eval/webapp/node_modules/mime/README.md +0 -90
- gaia/eval/webapp/node_modules/mime/cli.js +0 -8
- gaia/eval/webapp/node_modules/mime/mime.js +0 -108
- gaia/eval/webapp/node_modules/mime/package.json +0 -44
- gaia/eval/webapp/node_modules/mime/src/build.js +0 -53
- gaia/eval/webapp/node_modules/mime/src/test.js +0 -60
- gaia/eval/webapp/node_modules/mime/types.json +0 -1
- gaia/eval/webapp/node_modules/mime-db/HISTORY.md +0 -507
- gaia/eval/webapp/node_modules/mime-db/LICENSE +0 -23
- gaia/eval/webapp/node_modules/mime-db/README.md +0 -100
- gaia/eval/webapp/node_modules/mime-db/db.json +0 -8519
- gaia/eval/webapp/node_modules/mime-db/index.js +0 -12
- gaia/eval/webapp/node_modules/mime-db/package.json +0 -60
- gaia/eval/webapp/node_modules/mime-types/HISTORY.md +0 -397
- gaia/eval/webapp/node_modules/mime-types/LICENSE +0 -23
- gaia/eval/webapp/node_modules/mime-types/README.md +0 -113
- gaia/eval/webapp/node_modules/mime-types/index.js +0 -188
- gaia/eval/webapp/node_modules/mime-types/package.json +0 -44
- gaia/eval/webapp/node_modules/ms/index.js +0 -152
- gaia/eval/webapp/node_modules/ms/license.md +0 -21
- gaia/eval/webapp/node_modules/ms/package.json +0 -37
- gaia/eval/webapp/node_modules/ms/readme.md +0 -51
- gaia/eval/webapp/node_modules/negotiator/HISTORY.md +0 -108
- gaia/eval/webapp/node_modules/negotiator/LICENSE +0 -24
- gaia/eval/webapp/node_modules/negotiator/README.md +0 -203
- gaia/eval/webapp/node_modules/negotiator/index.js +0 -82
- gaia/eval/webapp/node_modules/negotiator/lib/charset.js +0 -169
- gaia/eval/webapp/node_modules/negotiator/lib/encoding.js +0 -184
- gaia/eval/webapp/node_modules/negotiator/lib/language.js +0 -179
- gaia/eval/webapp/node_modules/negotiator/lib/mediaType.js +0 -294
- gaia/eval/webapp/node_modules/negotiator/package.json +0 -42
- gaia/eval/webapp/node_modules/object-inspect/.eslintrc +0 -53
- gaia/eval/webapp/node_modules/object-inspect/.github/FUNDING.yml +0 -12
- gaia/eval/webapp/node_modules/object-inspect/.nycrc +0 -13
- gaia/eval/webapp/node_modules/object-inspect/CHANGELOG.md +0 -424
- gaia/eval/webapp/node_modules/object-inspect/LICENSE +0 -21
- gaia/eval/webapp/node_modules/object-inspect/example/all.js +0 -23
- gaia/eval/webapp/node_modules/object-inspect/example/circular.js +0 -6
- gaia/eval/webapp/node_modules/object-inspect/example/fn.js +0 -5
- gaia/eval/webapp/node_modules/object-inspect/example/inspect.js +0 -10
- gaia/eval/webapp/node_modules/object-inspect/index.js +0 -544
- gaia/eval/webapp/node_modules/object-inspect/package-support.json +0 -20
- gaia/eval/webapp/node_modules/object-inspect/package.json +0 -105
- gaia/eval/webapp/node_modules/object-inspect/readme.markdown +0 -84
- gaia/eval/webapp/node_modules/object-inspect/test/bigint.js +0 -58
- gaia/eval/webapp/node_modules/object-inspect/test/browser/dom.js +0 -15
- gaia/eval/webapp/node_modules/object-inspect/test/circular.js +0 -16
- gaia/eval/webapp/node_modules/object-inspect/test/deep.js +0 -12
- gaia/eval/webapp/node_modules/object-inspect/test/element.js +0 -53
- gaia/eval/webapp/node_modules/object-inspect/test/err.js +0 -48
- gaia/eval/webapp/node_modules/object-inspect/test/fakes.js +0 -29
- gaia/eval/webapp/node_modules/object-inspect/test/fn.js +0 -76
- gaia/eval/webapp/node_modules/object-inspect/test/global.js +0 -17
- gaia/eval/webapp/node_modules/object-inspect/test/has.js +0 -15
- gaia/eval/webapp/node_modules/object-inspect/test/holes.js +0 -15
- gaia/eval/webapp/node_modules/object-inspect/test/indent-option.js +0 -271
- gaia/eval/webapp/node_modules/object-inspect/test/inspect.js +0 -139
- gaia/eval/webapp/node_modules/object-inspect/test/lowbyte.js +0 -12
- gaia/eval/webapp/node_modules/object-inspect/test/number.js +0 -58
- gaia/eval/webapp/node_modules/object-inspect/test/quoteStyle.js +0 -26
- gaia/eval/webapp/node_modules/object-inspect/test/toStringTag.js +0 -40
- gaia/eval/webapp/node_modules/object-inspect/test/undef.js +0 -12
- gaia/eval/webapp/node_modules/object-inspect/test/values.js +0 -261
- gaia/eval/webapp/node_modules/object-inspect/test-core-js.js +0 -26
- gaia/eval/webapp/node_modules/object-inspect/util.inspect.js +0 -1
- gaia/eval/webapp/node_modules/on-finished/HISTORY.md +0 -98
- gaia/eval/webapp/node_modules/on-finished/LICENSE +0 -23
- gaia/eval/webapp/node_modules/on-finished/README.md +0 -162
- gaia/eval/webapp/node_modules/on-finished/index.js +0 -234
- gaia/eval/webapp/node_modules/on-finished/package.json +0 -39
- gaia/eval/webapp/node_modules/parseurl/HISTORY.md +0 -58
- gaia/eval/webapp/node_modules/parseurl/LICENSE +0 -24
- gaia/eval/webapp/node_modules/parseurl/README.md +0 -133
- gaia/eval/webapp/node_modules/parseurl/index.js +0 -158
- gaia/eval/webapp/node_modules/parseurl/package.json +0 -40
- gaia/eval/webapp/node_modules/path/.npmignore +0 -1
- gaia/eval/webapp/node_modules/path/LICENSE +0 -18
- gaia/eval/webapp/node_modules/path/README.md +0 -15
- gaia/eval/webapp/node_modules/path/package.json +0 -24
- gaia/eval/webapp/node_modules/path/path.js +0 -628
- gaia/eval/webapp/node_modules/path-to-regexp/LICENSE +0 -21
- gaia/eval/webapp/node_modules/path-to-regexp/Readme.md +0 -35
- gaia/eval/webapp/node_modules/path-to-regexp/index.js +0 -156
- gaia/eval/webapp/node_modules/path-to-regexp/package.json +0 -30
- gaia/eval/webapp/node_modules/process/.eslintrc +0 -21
- gaia/eval/webapp/node_modules/process/LICENSE +0 -22
- gaia/eval/webapp/node_modules/process/README.md +0 -26
- gaia/eval/webapp/node_modules/process/browser.js +0 -184
- gaia/eval/webapp/node_modules/process/index.js +0 -2
- gaia/eval/webapp/node_modules/process/package.json +0 -27
- gaia/eval/webapp/node_modules/process/test.js +0 -199
- gaia/eval/webapp/node_modules/proxy-addr/HISTORY.md +0 -161
- gaia/eval/webapp/node_modules/proxy-addr/LICENSE +0 -22
- gaia/eval/webapp/node_modules/proxy-addr/README.md +0 -139
- gaia/eval/webapp/node_modules/proxy-addr/index.js +0 -327
- gaia/eval/webapp/node_modules/proxy-addr/package.json +0 -47
- gaia/eval/webapp/node_modules/qs/.editorconfig +0 -46
- gaia/eval/webapp/node_modules/qs/.eslintrc +0 -38
- gaia/eval/webapp/node_modules/qs/.github/FUNDING.yml +0 -12
- gaia/eval/webapp/node_modules/qs/.nycrc +0 -13
- gaia/eval/webapp/node_modules/qs/CHANGELOG.md +0 -600
- gaia/eval/webapp/node_modules/qs/LICENSE.md +0 -29
- gaia/eval/webapp/node_modules/qs/README.md +0 -709
- gaia/eval/webapp/node_modules/qs/dist/qs.js +0 -90
- gaia/eval/webapp/node_modules/qs/lib/formats.js +0 -23
- gaia/eval/webapp/node_modules/qs/lib/index.js +0 -11
- gaia/eval/webapp/node_modules/qs/lib/parse.js +0 -296
- gaia/eval/webapp/node_modules/qs/lib/stringify.js +0 -351
- gaia/eval/webapp/node_modules/qs/lib/utils.js +0 -265
- gaia/eval/webapp/node_modules/qs/package.json +0 -91
- gaia/eval/webapp/node_modules/qs/test/empty-keys-cases.js +0 -267
- gaia/eval/webapp/node_modules/qs/test/parse.js +0 -1170
- gaia/eval/webapp/node_modules/qs/test/stringify.js +0 -1298
- gaia/eval/webapp/node_modules/qs/test/utils.js +0 -136
- gaia/eval/webapp/node_modules/range-parser/HISTORY.md +0 -56
- gaia/eval/webapp/node_modules/range-parser/LICENSE +0 -23
- gaia/eval/webapp/node_modules/range-parser/README.md +0 -84
- gaia/eval/webapp/node_modules/range-parser/index.js +0 -162
- gaia/eval/webapp/node_modules/range-parser/package.json +0 -44
- gaia/eval/webapp/node_modules/raw-body/HISTORY.md +0 -308
- gaia/eval/webapp/node_modules/raw-body/LICENSE +0 -22
- gaia/eval/webapp/node_modules/raw-body/README.md +0 -223
- gaia/eval/webapp/node_modules/raw-body/SECURITY.md +0 -24
- gaia/eval/webapp/node_modules/raw-body/index.d.ts +0 -87
- gaia/eval/webapp/node_modules/raw-body/index.js +0 -336
- gaia/eval/webapp/node_modules/raw-body/package.json +0 -49
- gaia/eval/webapp/node_modules/safe-buffer/LICENSE +0 -21
- gaia/eval/webapp/node_modules/safe-buffer/README.md +0 -584
- gaia/eval/webapp/node_modules/safe-buffer/index.d.ts +0 -187
- gaia/eval/webapp/node_modules/safe-buffer/index.js +0 -65
- gaia/eval/webapp/node_modules/safe-buffer/package.json +0 -51
- gaia/eval/webapp/node_modules/safer-buffer/LICENSE +0 -21
- gaia/eval/webapp/node_modules/safer-buffer/Porting-Buffer.md +0 -268
- gaia/eval/webapp/node_modules/safer-buffer/Readme.md +0 -156
- gaia/eval/webapp/node_modules/safer-buffer/dangerous.js +0 -58
- gaia/eval/webapp/node_modules/safer-buffer/package.json +0 -34
- gaia/eval/webapp/node_modules/safer-buffer/safer.js +0 -77
- gaia/eval/webapp/node_modules/safer-buffer/tests.js +0 -406
- gaia/eval/webapp/node_modules/send/HISTORY.md +0 -526
- gaia/eval/webapp/node_modules/send/LICENSE +0 -23
- gaia/eval/webapp/node_modules/send/README.md +0 -327
- gaia/eval/webapp/node_modules/send/SECURITY.md +0 -24
- gaia/eval/webapp/node_modules/send/index.js +0 -1142
- gaia/eval/webapp/node_modules/send/node_modules/encodeurl/HISTORY.md +0 -14
- gaia/eval/webapp/node_modules/send/node_modules/encodeurl/LICENSE +0 -22
- gaia/eval/webapp/node_modules/send/node_modules/encodeurl/README.md +0 -128
- gaia/eval/webapp/node_modules/send/node_modules/encodeurl/index.js +0 -60
- gaia/eval/webapp/node_modules/send/node_modules/encodeurl/package.json +0 -40
- gaia/eval/webapp/node_modules/send/node_modules/ms/index.js +0 -162
- gaia/eval/webapp/node_modules/send/node_modules/ms/license.md +0 -21
- gaia/eval/webapp/node_modules/send/node_modules/ms/package.json +0 -38
- gaia/eval/webapp/node_modules/send/node_modules/ms/readme.md +0 -59
- gaia/eval/webapp/node_modules/send/package.json +0 -62
- gaia/eval/webapp/node_modules/serve-static/HISTORY.md +0 -487
- gaia/eval/webapp/node_modules/serve-static/LICENSE +0 -25
- gaia/eval/webapp/node_modules/serve-static/README.md +0 -257
- gaia/eval/webapp/node_modules/serve-static/index.js +0 -209
- gaia/eval/webapp/node_modules/serve-static/package.json +0 -42
- gaia/eval/webapp/node_modules/setprototypeof/LICENSE +0 -13
- gaia/eval/webapp/node_modules/setprototypeof/README.md +0 -31
- gaia/eval/webapp/node_modules/setprototypeof/index.d.ts +0 -2
- gaia/eval/webapp/node_modules/setprototypeof/index.js +0 -17
- gaia/eval/webapp/node_modules/setprototypeof/package.json +0 -38
- gaia/eval/webapp/node_modules/setprototypeof/test/index.js +0 -24
- gaia/eval/webapp/node_modules/side-channel/.editorconfig +0 -9
- gaia/eval/webapp/node_modules/side-channel/.eslintrc +0 -12
- gaia/eval/webapp/node_modules/side-channel/.github/FUNDING.yml +0 -12
- gaia/eval/webapp/node_modules/side-channel/.nycrc +0 -13
- gaia/eval/webapp/node_modules/side-channel/CHANGELOG.md +0 -110
- gaia/eval/webapp/node_modules/side-channel/LICENSE +0 -21
- gaia/eval/webapp/node_modules/side-channel/README.md +0 -61
- gaia/eval/webapp/node_modules/side-channel/index.d.ts +0 -14
- gaia/eval/webapp/node_modules/side-channel/index.js +0 -43
- gaia/eval/webapp/node_modules/side-channel/package.json +0 -85
- gaia/eval/webapp/node_modules/side-channel/test/index.js +0 -104
- gaia/eval/webapp/node_modules/side-channel/tsconfig.json +0 -9
- gaia/eval/webapp/node_modules/side-channel-list/.editorconfig +0 -9
- gaia/eval/webapp/node_modules/side-channel-list/.eslintrc +0 -11
- gaia/eval/webapp/node_modules/side-channel-list/.github/FUNDING.yml +0 -12
- gaia/eval/webapp/node_modules/side-channel-list/.nycrc +0 -13
- gaia/eval/webapp/node_modules/side-channel-list/CHANGELOG.md +0 -15
- gaia/eval/webapp/node_modules/side-channel-list/LICENSE +0 -21
- gaia/eval/webapp/node_modules/side-channel-list/README.md +0 -62
- gaia/eval/webapp/node_modules/side-channel-list/index.d.ts +0 -13
- gaia/eval/webapp/node_modules/side-channel-list/index.js +0 -113
- gaia/eval/webapp/node_modules/side-channel-list/list.d.ts +0 -14
- gaia/eval/webapp/node_modules/side-channel-list/package.json +0 -77
- gaia/eval/webapp/node_modules/side-channel-list/test/index.js +0 -104
- gaia/eval/webapp/node_modules/side-channel-list/tsconfig.json +0 -9
- gaia/eval/webapp/node_modules/side-channel-map/.editorconfig +0 -9
- gaia/eval/webapp/node_modules/side-channel-map/.eslintrc +0 -11
- gaia/eval/webapp/node_modules/side-channel-map/.github/FUNDING.yml +0 -12
- gaia/eval/webapp/node_modules/side-channel-map/.nycrc +0 -13
- gaia/eval/webapp/node_modules/side-channel-map/CHANGELOG.md +0 -22
- gaia/eval/webapp/node_modules/side-channel-map/LICENSE +0 -21
- gaia/eval/webapp/node_modules/side-channel-map/README.md +0 -62
- gaia/eval/webapp/node_modules/side-channel-map/index.d.ts +0 -15
- gaia/eval/webapp/node_modules/side-channel-map/index.js +0 -68
- gaia/eval/webapp/node_modules/side-channel-map/package.json +0 -80
- gaia/eval/webapp/node_modules/side-channel-map/test/index.js +0 -114
- gaia/eval/webapp/node_modules/side-channel-map/tsconfig.json +0 -9
- gaia/eval/webapp/node_modules/side-channel-weakmap/.editorconfig +0 -9
- gaia/eval/webapp/node_modules/side-channel-weakmap/.eslintrc +0 -12
- gaia/eval/webapp/node_modules/side-channel-weakmap/.github/FUNDING.yml +0 -12
- gaia/eval/webapp/node_modules/side-channel-weakmap/.nycrc +0 -13
- gaia/eval/webapp/node_modules/side-channel-weakmap/CHANGELOG.md +0 -28
- gaia/eval/webapp/node_modules/side-channel-weakmap/LICENSE +0 -21
- gaia/eval/webapp/node_modules/side-channel-weakmap/README.md +0 -62
- gaia/eval/webapp/node_modules/side-channel-weakmap/index.d.ts +0 -15
- gaia/eval/webapp/node_modules/side-channel-weakmap/index.js +0 -84
- gaia/eval/webapp/node_modules/side-channel-weakmap/package.json +0 -87
- gaia/eval/webapp/node_modules/side-channel-weakmap/test/index.js +0 -114
- gaia/eval/webapp/node_modules/side-channel-weakmap/tsconfig.json +0 -9
- gaia/eval/webapp/node_modules/statuses/HISTORY.md +0 -82
- gaia/eval/webapp/node_modules/statuses/LICENSE +0 -23
- gaia/eval/webapp/node_modules/statuses/README.md +0 -136
- gaia/eval/webapp/node_modules/statuses/codes.json +0 -65
- gaia/eval/webapp/node_modules/statuses/index.js +0 -146
- gaia/eval/webapp/node_modules/statuses/package.json +0 -49
- gaia/eval/webapp/node_modules/toidentifier/HISTORY.md +0 -9
- gaia/eval/webapp/node_modules/toidentifier/LICENSE +0 -21
- gaia/eval/webapp/node_modules/toidentifier/README.md +0 -61
- gaia/eval/webapp/node_modules/toidentifier/index.js +0 -32
- gaia/eval/webapp/node_modules/toidentifier/package.json +0 -38
- gaia/eval/webapp/node_modules/type-is/HISTORY.md +0 -259
- gaia/eval/webapp/node_modules/type-is/LICENSE +0 -23
- gaia/eval/webapp/node_modules/type-is/README.md +0 -170
- gaia/eval/webapp/node_modules/type-is/index.js +0 -266
- gaia/eval/webapp/node_modules/type-is/package.json +0 -45
- gaia/eval/webapp/node_modules/unpipe/HISTORY.md +0 -4
- gaia/eval/webapp/node_modules/unpipe/LICENSE +0 -22
- gaia/eval/webapp/node_modules/unpipe/README.md +0 -43
- gaia/eval/webapp/node_modules/unpipe/index.js +0 -69
- gaia/eval/webapp/node_modules/unpipe/package.json +0 -27
- gaia/eval/webapp/node_modules/util/LICENSE +0 -18
- gaia/eval/webapp/node_modules/util/README.md +0 -15
- gaia/eval/webapp/node_modules/util/node_modules/inherits/LICENSE +0 -16
- gaia/eval/webapp/node_modules/util/node_modules/inherits/README.md +0 -42
- gaia/eval/webapp/node_modules/util/node_modules/inherits/inherits.js +0 -7
- gaia/eval/webapp/node_modules/util/node_modules/inherits/inherits_browser.js +0 -23
- gaia/eval/webapp/node_modules/util/node_modules/inherits/package.json +0 -29
- gaia/eval/webapp/node_modules/util/package.json +0 -35
- gaia/eval/webapp/node_modules/util/support/isBuffer.js +0 -3
- gaia/eval/webapp/node_modules/util/support/isBufferBrowser.js +0 -6
- gaia/eval/webapp/node_modules/util/util.js +0 -586
- gaia/eval/webapp/node_modules/utils-merge/.npmignore +0 -9
- gaia/eval/webapp/node_modules/utils-merge/LICENSE +0 -20
- gaia/eval/webapp/node_modules/utils-merge/README.md +0 -34
- gaia/eval/webapp/node_modules/utils-merge/index.js +0 -23
- gaia/eval/webapp/node_modules/utils-merge/package.json +0 -40
- gaia/eval/webapp/node_modules/vary/HISTORY.md +0 -39
- gaia/eval/webapp/node_modules/vary/LICENSE +0 -22
- gaia/eval/webapp/node_modules/vary/README.md +0 -101
- gaia/eval/webapp/node_modules/vary/index.js +0 -149
- gaia/eval/webapp/node_modules/vary/package.json +0 -43
- {amd_gaia-0.14.2.dist-info → amd_gaia-0.14.3.dist-info}/WHEEL +0 -0
- {amd_gaia-0.14.2.dist-info → amd_gaia-0.14.3.dist-info}/licenses/LICENSE.md +0 -0
- {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
|
+
)
|