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