From 6e733683f85d04734a45ccde78b2bd54ba886636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=AD=99=E5=B0=8F=E4=BA=91?= Date: Thu, 4 Dec 2025 14:48:38 +0800 Subject: [PATCH] first commit --- README.md | 464 ++ backend/.env | 90 + backend/.env.example | 89 + backend/TH-Agenter.db | Bin 0 -> 376832 bytes backend/configs/settings.yaml | 49 + backend/data/logs/app.log | 3668 ++++++++++++++ backend/requirements.txt | 77 + backend/tests/fastapi_test/main.py | 153 + backend/tests/fastapi_test/test_main.py | 93 + backend/tests/init_db.py | 100 + backend/tests/pandas_test.py | 125 + backend/th_agenter/__init__.py | 12 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 545 bytes .../__pycache__/main.cpython-313.pyc | Bin 0 -> 1459 bytes backend/th_agenter/api/__init__.py | 1 + .../api/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 201 bytes .../api/__pycache__/routes.cpython-313.pyc | Bin 0 -> 2152 bytes backend/th_agenter/api/endpoints/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 214 bytes .../__pycache__/auth.cpython-313.pyc | Bin 0 -> 5117 bytes .../__pycache__/chat.cpython-313.pyc | Bin 0 -> 9346 bytes .../database_config.cpython-313.pyc | Bin 0 -> 10304 bytes .../knowledge_base.cpython-313.pyc | Bin 0 -> 23445 bytes .../__pycache__/llm_configs.cpython-313.pyc | Bin 0 -> 22527 bytes .../__pycache__/roles.cpython-313.pyc | Bin 0 -> 14747 bytes .../__pycache__/smart_chat.cpython-313.pyc | Bin 0 -> 13829 bytes .../__pycache__/smart_query.cpython-313.pyc | Bin 0 -> 30956 bytes .../table_metadata.cpython-313.pyc | Bin 0 -> 11424 bytes .../__pycache__/users.cpython-313.pyc | Bin 0 -> 9101 bytes .../__pycache__/workflow.cpython-313.pyc | Bin 0 -> 23005 bytes backend/th_agenter/api/endpoints/auth.py | 125 + backend/th_agenter/api/endpoints/chat.py | 237 + .../api/endpoints/database_config.py | 207 + .../api/endpoints/knowledge_base.py | 666 +++ .../th_agenter/api/endpoints/llm_configs.py | 528 ++ backend/th_agenter/api/endpoints/roles.py | 346 ++ .../th_agenter/api/endpoints/smart_chat.py | 342 ++ .../th_agenter/api/endpoints/smart_query.py | 754 +++ .../api/endpoints/table_metadata.py | 248 + backend/th_agenter/api/endpoints/users.py | 241 + backend/th_agenter/api/endpoints/workflow.py | 538 ++ backend/th_agenter/api/routes.py | 101 + backend/th_agenter/core/__init__.py | 1 + .../core/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 203 bytes .../core/__pycache__/app.cpython-313.pyc | Bin 0 -> 7288 bytes .../core/__pycache__/config.cpython-313.pyc | Bin 0 -> 21359 bytes .../core/__pycache__/context.cpython-313.pyc | Bin 0 -> 5646 bytes .../core/__pycache__/llm.cpython-313.pyc | Bin 0 -> 2601 bytes .../core/__pycache__/logging.cpython-313.pyc | Bin 0 -> 3375 bytes .../__pycache__/middleware.cpython-313.pyc | Bin 0 -> 6774 bytes .../simple_permissions.cpython-313.pyc | Bin 0 -> 3856 bytes backend/th_agenter/core/app.py | 177 + backend/th_agenter/core/config.py | 482 ++ backend/th_agenter/core/context.py | 120 + backend/th_agenter/core/exceptions.py | 52 + backend/th_agenter/core/llm.py | 47 + backend/th_agenter/core/logging.py | 64 + backend/th_agenter/core/middleware.py | 163 + backend/th_agenter/core/simple_permissions.py | 90 + backend/th_agenter/core/user_utils.py | 76 + backend/th_agenter/db/__init__.py | 6 + .../db/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 341 bytes .../db/__pycache__/base.cpython-313.pyc | Bin 0 -> 2752 bytes .../db/__pycache__/database.cpython-313.pyc | Bin 0 -> 3374 bytes .../init_system_data.cpython-313.pyc | Bin 0 -> 4229 bytes backend/th_agenter/db/base.py | 62 + backend/th_agenter/db/database.py | 89 + backend/th_agenter/db/db_config_key.key | 1 + backend/th_agenter/db/init_system_data.py | 121 + .../db/migrations/add_system_management.py | 216 + .../migrations/add_user_department_table.py | 83 + .../migrations/migrate_hardcoded_resources.py | 451 ++ .../db/migrations/remove_permission_tables.py | 146 + backend/th_agenter/main.py | 35 + backend/th_agenter/models/__init__.py | 27 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 796 bytes .../__pycache__/agent_config.cpython-313.pyc | Bin 0 -> 2392 bytes .../__pycache__/conversation.cpython-313.pyc | Bin 0 -> 2162 bytes .../database_config.cpython-313.pyc | Bin 0 -> 2526 bytes .../__pycache__/excel_file.cpython-313.pyc | Bin 0 -> 4211 bytes .../knowledge_base.cpython-313.pyc | Bin 0 -> 4695 bytes .../__pycache__/llm_config.cpython-313.pyc | Bin 0 -> 7259 bytes .../__pycache__/message.cpython-313.pyc | Bin 0 -> 3525 bytes .../__pycache__/permission.cpython-313.pyc | Bin 0 -> 2790 bytes .../table_metadata.cpython-313.pyc | Bin 0 -> 3082 bytes .../models/__pycache__/user.cpython-313.pyc | Bin 0 -> 5054 bytes .../__pycache__/workflow.cpython-313.pyc | Bin 0 -> 8239 bytes backend/th_agenter/models/agent_config.py | 53 + backend/th_agenter/models/base.py | 5 + backend/th_agenter/models/conversation.py | 38 + backend/th_agenter/models/database_config.py | 52 + backend/th_agenter/models/excel_file.py | 89 + backend/th_agenter/models/knowledge_base.py | 92 + backend/th_agenter/models/llm_config.py | 165 + backend/th_agenter/models/message.py | 69 + backend/th_agenter/models/permission.py | 53 + backend/th_agenter/models/table_metadata.py | 61 + backend/th_agenter/models/user.py | 92 + backend/th_agenter/models/workflow.py | 175 + backend/th_agenter/schemas/__init__.py | 16 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 489 bytes .../__pycache__/llm_config.cpython-313.pyc | Bin 0 -> 8664 bytes .../__pycache__/permission.cpython-313.pyc | Bin 0 -> 4565 bytes .../schemas/__pycache__/user.cpython-313.pyc | Bin 0 -> 3964 bytes .../__pycache__/workflow.cpython-313.pyc | Bin 0 -> 11812 bytes backend/th_agenter/schemas/llm_config.py | 152 + backend/th_agenter/schemas/permission.py | 68 + backend/th_agenter/schemas/user.py | 61 + backend/th_agenter/schemas/workflow.py | 231 + .../__pycache__/init_system.cpython-313.pyc | Bin 0 -> 5232 bytes backend/th_agenter/scripts/init_system.py | 104 + backend/th_agenter/services/__init__.py | 1 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 201 bytes .../__pycache__/agent_config.cpython-313.pyc | Bin 0 -> 11105 bytes .../services/__pycache__/auth.cpython-313.pyc | Bin 0 -> 7723 bytes .../services/__pycache__/chat.cpython-313.pyc | Bin 0 -> 13994 bytes .../__pycache__/conversation.cpython-313.pyc | Bin 0 -> 12864 bytes .../conversation_context.cpython-313.pyc | Bin 0 -> 11162 bytes .../database_config_service.cpython-313.pyc | Bin 0 -> 15423 bytes .../__pycache__/document.cpython-313.pyc | Bin 0 -> 15350 bytes .../document_processor.cpython-313.pyc | Bin 0 -> 42476 bytes .../embedding_factory.cpython-313.pyc | Bin 0 -> 4713 bytes .../excel_metadata_service.cpython-313.pyc | Bin 0 -> 11472 bytes .../knowledge_base.cpython-313.pyc | Bin 0 -> 9712 bytes .../knowledge_chat.cpython-313.pyc | Bin 0 -> 15588 bytes .../langchain_chat.cpython-313.pyc | Bin 0 -> 16015 bytes .../llm_config_service.cpython-313.pyc | Bin 0 -> 6633 bytes .../__pycache__/llm_service.cpython-313.pyc | Bin 0 -> 5180 bytes .../mysql_tool_manager.cpython-313.pyc | Bin 0 -> 2466 bytes .../postgresql_tool_manager.cpython-313.pyc | Bin 0 -> 2580 bytes .../smart_db_workflow.cpython-313.pyc | Bin 0 -> 34390 bytes .../smart_excel_workflow.cpython-313.pyc | Bin 0 -> 54391 bytes .../__pycache__/smart_query.cpython-313.pyc | Bin 0 -> 30404 bytes .../smart_workflow.cpython-313.pyc | Bin 0 -> 3550 bytes .../__pycache__/storage.cpython-313.pyc | Bin 0 -> 13479 bytes .../table_metadata_service.cpython-313.pyc | Bin 0 -> 16906 bytes .../services/__pycache__/user.cpython-313.pyc | Bin 0 -> 14743 bytes .../workflow_engine.cpython-313.pyc | Bin 0 -> 35271 bytes .../zhipu_embeddings.cpython-313.pyc | Bin 0 -> 4239 bytes backend/th_agenter/services/agent/__init__.py | 10 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 371 bytes .../__pycache__/agent_service.cpython-313.pyc | Bin 0 -> 21488 bytes .../agent/__pycache__/base.cpython-313.pyc | Bin 0 -> 11946 bytes .../langgraph_agent_service.cpython-313.pyc | Bin 0 -> 18068 bytes .../services/agent/agent_service.py | 468 ++ backend/th_agenter/services/agent/base.py | 248 + .../services/agent/langgraph_agent_service.py | 441 ++ backend/th_agenter/services/agent_config.py | 206 + backend/th_agenter/services/auth.py | 141 + backend/th_agenter/services/chat.py | 363 ++ backend/th_agenter/services/conversation.py | 260 + .../services/conversation_context.py | 310 ++ .../services/database_config_service.py | 324 ++ backend/th_agenter/services/document.py | 301 ++ .../th_agenter/services/document_processor.py | 1005 ++++ .../th_agenter/services/embedding_factory.py | 96 + .../services/excel_metadata_service.py | 241 + backend/th_agenter/services/knowledge_base.py | 167 + backend/th_agenter/services/knowledge_chat.py | 387 ++ backend/th_agenter/services/langchain_chat.py | 383 ++ .../th_agenter/services/llm_config_service.py | 121 + backend/th_agenter/services/llm_service.py | 113 + backend/th_agenter/services/mcp/__init__.py | 0 .../mcp/__pycache__/__init__.cpython-313.pyc | Bin 0 -> 173 bytes .../mcp/__pycache__/mysql_mcp.cpython-313.pyc | Bin 0 -> 15686 bytes .../postgresql_mcp.cpython-313.pyc | Bin 0 -> 13818 bytes backend/th_agenter/services/mcp/mysql_mcp.py | 458 ++ .../th_agenter/services/mcp/postgresql_mcp.py | 389 ++ .../th_agenter/services/mysql_tool_manager.py | 51 + .../services/postgresql_tool_manager.py | 51 + .../th_agenter/services/smart_db_workflow.py | 879 ++++ .../services/smart_excel_workflow.py | 1391 +++++ backend/th_agenter/services/smart_query.py | 757 +++ backend/th_agenter/services/smart_workflow.py | 83 + backend/th_agenter/services/storage.py | 299 ++ .../services/table_metadata_service.py | 446 ++ backend/th_agenter/services/tools/__init__.py | 36 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 988 bytes .../__pycache__/datetime_tool.cpython-313.pyc | Bin 0 -> 7884 bytes .../tools/__pycache__/search.cpython-313.pyc | Bin 0 -> 4562 bytes .../tools/__pycache__/weather.cpython-313.pyc | Bin 0 -> 4635 bytes .../services/tools/datetime_tool.py | 186 + backend/th_agenter/services/tools/search.py | 92 + backend/th_agenter/services/tools/weather.py | 85 + backend/th_agenter/services/user.py | 282 ++ .../th_agenter/services/workflow_engine.py | 924 ++++ .../th_agenter/services/zhipu_embeddings.py | 74 + backend/th_agenter/utils/__init__.py | 30 + .../__pycache__/__init__.cpython-313.pyc | Bin 0 -> 698 bytes .../__pycache__/exceptions.cpython-313.pyc | Bin 0 -> 8476 bytes .../__pycache__/file_utils.cpython-313.pyc | Bin 0 -> 8709 bytes .../utils/__pycache__/logger.cpython-313.pyc | Bin 0 -> 2397 bytes .../utils/__pycache__/schemas.cpython-313.pyc | Bin 0 -> 19324 bytes backend/th_agenter/utils/exceptions.py | 170 + backend/th_agenter/utils/file_utils.py | 191 + backend/th_agenter/utils/logger.py | 68 + backend/th_agenter/utils/node_parameters.py | 217 + backend/th_agenter/utils/schemas.py | 375 ++ frontend/.env | 19 + frontend/.env.development | 16 + frontend/.env.example | 19 + frontend/auto-imports.d.ts | 89 + frontend/components.d.ts | 70 + frontend/env.d.ts | 14 + frontend/index.html | 107 + frontend/package-lock.json | 4493 +++++++++++++++++ frontend/package.json | 46 + frontend/src/App.vue | 111 + frontend/src/api/auth.ts | 35 + frontend/src/api/chat.ts | 109 + frontend/src/api/departments.ts | 97 + frontend/src/api/index.ts | 14 + frontend/src/api/knowledge.ts | 71 + frontend/src/api/llmConfig.ts | 156 + frontend/src/api/request.ts | 146 + frontend/src/api/roles.ts | 80 + frontend/src/api/userDepartments.ts | 114 + frontend/src/api/users.ts | 81 + frontend/src/api/workflow.ts | 166 + frontend/src/assets/logo.png | Bin 0 -> 8952 bytes frontend/src/components/AgentManagement.vue | 1430 ++++++ frontend/src/components/AgentWorkflow.vue | 845 ++++ frontend/src/components/CreativeStudio.vue | 1151 +++++ .../src/components/KnowledgeManagement.vue | 1726 +++++++ frontend/src/components/MainLayout.vue | 1019 ++++ .../src/components/NodeParameterConfig.vue | 894 ++++ frontend/src/components/NotFound.vue | 176 + .../src/components/ParameterInputDialog.vue | 427 ++ frontend/src/components/ProfileDialog.vue | 315 ++ frontend/src/components/SmartQuery.vue | 4062 +++++++++++++++ frontend/src/components/WorkflowEditor.vue | 3967 +++++++++++++++ .../system/DepartmentManagement.vue | 1009 ++++ .../components/system/LLMConfigManagement.vue | 1909 +++++++ .../src/components/system/RoleManagement.vue | 1076 ++++ .../src/components/system/UserManagement.vue | 1011 ++++ frontend/src/main.ts | 23 + frontend/src/router/index.ts | 182 + frontend/src/services/sse.ts | 290 ++ frontend/src/stores/chat.ts | 704 +++ frontend/src/stores/index.ts | 4 + frontend/src/stores/knowledge.ts | 312 ++ frontend/src/stores/menu.ts | 113 + frontend/src/stores/user.ts | 241 + frontend/src/styles/index.scss | 229 + frontend/src/types/index.ts | 440 ++ frontend/src/utils/date.ts | 72 + frontend/src/utils/index.ts | 315 ++ frontend/src/views/Chat.vue | 1903 +++++++ frontend/src/views/Flow/WorkflowList.vue | 563 +++ frontend/src/views/Knowledge.vue | 724 +++ frontend/src/views/Login.vue | 184 + frontend/src/views/MCPServiceManagement.vue | 676 +++ frontend/src/views/Profile.vue | 374 ++ frontend/src/views/Register.vue | 203 + frontend/src/views/SystemManagement.vue | 56 + frontend/tsconfig.json | 25 + frontend/vite.config.ts | 85 + 257 files changed, 61248 insertions(+) create mode 100644 README.md create mode 100644 backend/.env create mode 100644 backend/.env.example create mode 100644 backend/TH-Agenter.db create mode 100644 backend/configs/settings.yaml create mode 100644 backend/data/logs/app.log create mode 100644 backend/requirements.txt create mode 100644 backend/tests/fastapi_test/main.py create mode 100644 backend/tests/fastapi_test/test_main.py create mode 100644 backend/tests/init_db.py create mode 100644 backend/tests/pandas_test.py create mode 100644 backend/th_agenter/__init__.py create mode 100644 backend/th_agenter/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/th_agenter/__pycache__/main.cpython-313.pyc create mode 100644 backend/th_agenter/api/__init__.py create mode 100644 backend/th_agenter/api/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/th_agenter/api/__pycache__/routes.cpython-313.pyc create mode 100644 backend/th_agenter/api/endpoints/__init__.py create mode 100644 backend/th_agenter/api/endpoints/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/th_agenter/api/endpoints/__pycache__/auth.cpython-313.pyc create mode 100644 backend/th_agenter/api/endpoints/__pycache__/chat.cpython-313.pyc create mode 100644 backend/th_agenter/api/endpoints/__pycache__/database_config.cpython-313.pyc create mode 100644 backend/th_agenter/api/endpoints/__pycache__/knowledge_base.cpython-313.pyc create mode 100644 backend/th_agenter/api/endpoints/__pycache__/llm_configs.cpython-313.pyc create mode 100644 backend/th_agenter/api/endpoints/__pycache__/roles.cpython-313.pyc create mode 100644 backend/th_agenter/api/endpoints/__pycache__/smart_chat.cpython-313.pyc create mode 100644 backend/th_agenter/api/endpoints/__pycache__/smart_query.cpython-313.pyc create mode 100644 backend/th_agenter/api/endpoints/__pycache__/table_metadata.cpython-313.pyc create mode 100644 backend/th_agenter/api/endpoints/__pycache__/users.cpython-313.pyc create mode 100644 backend/th_agenter/api/endpoints/__pycache__/workflow.cpython-313.pyc create mode 100644 backend/th_agenter/api/endpoints/auth.py create mode 100644 backend/th_agenter/api/endpoints/chat.py create mode 100644 backend/th_agenter/api/endpoints/database_config.py create mode 100644 backend/th_agenter/api/endpoints/knowledge_base.py create mode 100644 backend/th_agenter/api/endpoints/llm_configs.py create mode 100644 backend/th_agenter/api/endpoints/roles.py create mode 100644 backend/th_agenter/api/endpoints/smart_chat.py create mode 100644 backend/th_agenter/api/endpoints/smart_query.py create mode 100644 backend/th_agenter/api/endpoints/table_metadata.py create mode 100644 backend/th_agenter/api/endpoints/users.py create mode 100644 backend/th_agenter/api/endpoints/workflow.py create mode 100644 backend/th_agenter/api/routes.py create mode 100644 backend/th_agenter/core/__init__.py create mode 100644 backend/th_agenter/core/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/th_agenter/core/__pycache__/app.cpython-313.pyc create mode 100644 backend/th_agenter/core/__pycache__/config.cpython-313.pyc create mode 100644 backend/th_agenter/core/__pycache__/context.cpython-313.pyc create mode 100644 backend/th_agenter/core/__pycache__/llm.cpython-313.pyc create mode 100644 backend/th_agenter/core/__pycache__/logging.cpython-313.pyc create mode 100644 backend/th_agenter/core/__pycache__/middleware.cpython-313.pyc create mode 100644 backend/th_agenter/core/__pycache__/simple_permissions.cpython-313.pyc create mode 100644 backend/th_agenter/core/app.py create mode 100644 backend/th_agenter/core/config.py create mode 100644 backend/th_agenter/core/context.py create mode 100644 backend/th_agenter/core/exceptions.py create mode 100644 backend/th_agenter/core/llm.py create mode 100644 backend/th_agenter/core/logging.py create mode 100644 backend/th_agenter/core/middleware.py create mode 100644 backend/th_agenter/core/simple_permissions.py create mode 100644 backend/th_agenter/core/user_utils.py create mode 100644 backend/th_agenter/db/__init__.py create mode 100644 backend/th_agenter/db/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/th_agenter/db/__pycache__/base.cpython-313.pyc create mode 100644 backend/th_agenter/db/__pycache__/database.cpython-313.pyc create mode 100644 backend/th_agenter/db/__pycache__/init_system_data.cpython-313.pyc create mode 100644 backend/th_agenter/db/base.py create mode 100644 backend/th_agenter/db/database.py create mode 100644 backend/th_agenter/db/db_config_key.key create mode 100644 backend/th_agenter/db/init_system_data.py create mode 100644 backend/th_agenter/db/migrations/add_system_management.py create mode 100644 backend/th_agenter/db/migrations/add_user_department_table.py create mode 100644 backend/th_agenter/db/migrations/migrate_hardcoded_resources.py create mode 100644 backend/th_agenter/db/migrations/remove_permission_tables.py create mode 100644 backend/th_agenter/main.py create mode 100644 backend/th_agenter/models/__init__.py create mode 100644 backend/th_agenter/models/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/th_agenter/models/__pycache__/agent_config.cpython-313.pyc create mode 100644 backend/th_agenter/models/__pycache__/conversation.cpython-313.pyc create mode 100644 backend/th_agenter/models/__pycache__/database_config.cpython-313.pyc create mode 100644 backend/th_agenter/models/__pycache__/excel_file.cpython-313.pyc create mode 100644 backend/th_agenter/models/__pycache__/knowledge_base.cpython-313.pyc create mode 100644 backend/th_agenter/models/__pycache__/llm_config.cpython-313.pyc create mode 100644 backend/th_agenter/models/__pycache__/message.cpython-313.pyc create mode 100644 backend/th_agenter/models/__pycache__/permission.cpython-313.pyc create mode 100644 backend/th_agenter/models/__pycache__/table_metadata.cpython-313.pyc create mode 100644 backend/th_agenter/models/__pycache__/user.cpython-313.pyc create mode 100644 backend/th_agenter/models/__pycache__/workflow.cpython-313.pyc create mode 100644 backend/th_agenter/models/agent_config.py create mode 100644 backend/th_agenter/models/base.py create mode 100644 backend/th_agenter/models/conversation.py create mode 100644 backend/th_agenter/models/database_config.py create mode 100644 backend/th_agenter/models/excel_file.py create mode 100644 backend/th_agenter/models/knowledge_base.py create mode 100644 backend/th_agenter/models/llm_config.py create mode 100644 backend/th_agenter/models/message.py create mode 100644 backend/th_agenter/models/permission.py create mode 100644 backend/th_agenter/models/table_metadata.py create mode 100644 backend/th_agenter/models/user.py create mode 100644 backend/th_agenter/models/workflow.py create mode 100644 backend/th_agenter/schemas/__init__.py create mode 100644 backend/th_agenter/schemas/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/th_agenter/schemas/__pycache__/llm_config.cpython-313.pyc create mode 100644 backend/th_agenter/schemas/__pycache__/permission.cpython-313.pyc create mode 100644 backend/th_agenter/schemas/__pycache__/user.cpython-313.pyc create mode 100644 backend/th_agenter/schemas/__pycache__/workflow.cpython-313.pyc create mode 100644 backend/th_agenter/schemas/llm_config.py create mode 100644 backend/th_agenter/schemas/permission.py create mode 100644 backend/th_agenter/schemas/user.py create mode 100644 backend/th_agenter/schemas/workflow.py create mode 100644 backend/th_agenter/scripts/__pycache__/init_system.cpython-313.pyc create mode 100644 backend/th_agenter/scripts/init_system.py create mode 100644 backend/th_agenter/services/__init__.py create mode 100644 backend/th_agenter/services/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/agent_config.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/auth.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/chat.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/conversation.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/conversation_context.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/database_config_service.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/document.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/document_processor.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/embedding_factory.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/excel_metadata_service.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/knowledge_base.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/knowledge_chat.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/langchain_chat.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/llm_config_service.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/llm_service.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/mysql_tool_manager.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/postgresql_tool_manager.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/smart_db_workflow.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/smart_excel_workflow.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/smart_query.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/smart_workflow.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/storage.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/table_metadata_service.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/user.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/workflow_engine.cpython-313.pyc create mode 100644 backend/th_agenter/services/__pycache__/zhipu_embeddings.cpython-313.pyc create mode 100644 backend/th_agenter/services/agent/__init__.py create mode 100644 backend/th_agenter/services/agent/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/th_agenter/services/agent/__pycache__/agent_service.cpython-313.pyc create mode 100644 backend/th_agenter/services/agent/__pycache__/base.cpython-313.pyc create mode 100644 backend/th_agenter/services/agent/__pycache__/langgraph_agent_service.cpython-313.pyc create mode 100644 backend/th_agenter/services/agent/agent_service.py create mode 100644 backend/th_agenter/services/agent/base.py create mode 100644 backend/th_agenter/services/agent/langgraph_agent_service.py create mode 100644 backend/th_agenter/services/agent_config.py create mode 100644 backend/th_agenter/services/auth.py create mode 100644 backend/th_agenter/services/chat.py create mode 100644 backend/th_agenter/services/conversation.py create mode 100644 backend/th_agenter/services/conversation_context.py create mode 100644 backend/th_agenter/services/database_config_service.py create mode 100644 backend/th_agenter/services/document.py create mode 100644 backend/th_agenter/services/document_processor.py create mode 100644 backend/th_agenter/services/embedding_factory.py create mode 100644 backend/th_agenter/services/excel_metadata_service.py create mode 100644 backend/th_agenter/services/knowledge_base.py create mode 100644 backend/th_agenter/services/knowledge_chat.py create mode 100644 backend/th_agenter/services/langchain_chat.py create mode 100644 backend/th_agenter/services/llm_config_service.py create mode 100644 backend/th_agenter/services/llm_service.py create mode 100644 backend/th_agenter/services/mcp/__init__.py create mode 100644 backend/th_agenter/services/mcp/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/th_agenter/services/mcp/__pycache__/mysql_mcp.cpython-313.pyc create mode 100644 backend/th_agenter/services/mcp/__pycache__/postgresql_mcp.cpython-313.pyc create mode 100644 backend/th_agenter/services/mcp/mysql_mcp.py create mode 100644 backend/th_agenter/services/mcp/postgresql_mcp.py create mode 100644 backend/th_agenter/services/mysql_tool_manager.py create mode 100644 backend/th_agenter/services/postgresql_tool_manager.py create mode 100644 backend/th_agenter/services/smart_db_workflow.py create mode 100644 backend/th_agenter/services/smart_excel_workflow.py create mode 100644 backend/th_agenter/services/smart_query.py create mode 100644 backend/th_agenter/services/smart_workflow.py create mode 100644 backend/th_agenter/services/storage.py create mode 100644 backend/th_agenter/services/table_metadata_service.py create mode 100644 backend/th_agenter/services/tools/__init__.py create mode 100644 backend/th_agenter/services/tools/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/th_agenter/services/tools/__pycache__/datetime_tool.cpython-313.pyc create mode 100644 backend/th_agenter/services/tools/__pycache__/search.cpython-313.pyc create mode 100644 backend/th_agenter/services/tools/__pycache__/weather.cpython-313.pyc create mode 100644 backend/th_agenter/services/tools/datetime_tool.py create mode 100644 backend/th_agenter/services/tools/search.py create mode 100644 backend/th_agenter/services/tools/weather.py create mode 100644 backend/th_agenter/services/user.py create mode 100644 backend/th_agenter/services/workflow_engine.py create mode 100644 backend/th_agenter/services/zhipu_embeddings.py create mode 100644 backend/th_agenter/utils/__init__.py create mode 100644 backend/th_agenter/utils/__pycache__/__init__.cpython-313.pyc create mode 100644 backend/th_agenter/utils/__pycache__/exceptions.cpython-313.pyc create mode 100644 backend/th_agenter/utils/__pycache__/file_utils.cpython-313.pyc create mode 100644 backend/th_agenter/utils/__pycache__/logger.cpython-313.pyc create mode 100644 backend/th_agenter/utils/__pycache__/schemas.cpython-313.pyc create mode 100644 backend/th_agenter/utils/exceptions.py create mode 100644 backend/th_agenter/utils/file_utils.py create mode 100644 backend/th_agenter/utils/logger.py create mode 100644 backend/th_agenter/utils/node_parameters.py create mode 100644 backend/th_agenter/utils/schemas.py create mode 100644 frontend/.env create mode 100644 frontend/.env.development create mode 100644 frontend/.env.example create mode 100644 frontend/auto-imports.d.ts create mode 100644 frontend/components.d.ts create mode 100644 frontend/env.d.ts create mode 100644 frontend/index.html create mode 100644 frontend/package-lock.json create mode 100644 frontend/package.json create mode 100644 frontend/src/App.vue create mode 100644 frontend/src/api/auth.ts create mode 100644 frontend/src/api/chat.ts create mode 100644 frontend/src/api/departments.ts create mode 100644 frontend/src/api/index.ts create mode 100644 frontend/src/api/knowledge.ts create mode 100644 frontend/src/api/llmConfig.ts create mode 100644 frontend/src/api/request.ts create mode 100644 frontend/src/api/roles.ts create mode 100644 frontend/src/api/userDepartments.ts create mode 100644 frontend/src/api/users.ts create mode 100644 frontend/src/api/workflow.ts create mode 100644 frontend/src/assets/logo.png create mode 100644 frontend/src/components/AgentManagement.vue create mode 100644 frontend/src/components/AgentWorkflow.vue create mode 100644 frontend/src/components/CreativeStudio.vue create mode 100644 frontend/src/components/KnowledgeManagement.vue create mode 100644 frontend/src/components/MainLayout.vue create mode 100644 frontend/src/components/NodeParameterConfig.vue create mode 100644 frontend/src/components/NotFound.vue create mode 100644 frontend/src/components/ParameterInputDialog.vue create mode 100644 frontend/src/components/ProfileDialog.vue create mode 100644 frontend/src/components/SmartQuery.vue create mode 100644 frontend/src/components/WorkflowEditor.vue create mode 100644 frontend/src/components/system/DepartmentManagement.vue create mode 100644 frontend/src/components/system/LLMConfigManagement.vue create mode 100644 frontend/src/components/system/RoleManagement.vue create mode 100644 frontend/src/components/system/UserManagement.vue create mode 100644 frontend/src/main.ts create mode 100644 frontend/src/router/index.ts create mode 100644 frontend/src/services/sse.ts create mode 100644 frontend/src/stores/chat.ts create mode 100644 frontend/src/stores/index.ts create mode 100644 frontend/src/stores/knowledge.ts create mode 100644 frontend/src/stores/menu.ts create mode 100644 frontend/src/stores/user.ts create mode 100644 frontend/src/styles/index.scss create mode 100644 frontend/src/types/index.ts create mode 100644 frontend/src/utils/date.ts create mode 100644 frontend/src/utils/index.ts create mode 100644 frontend/src/views/Chat.vue create mode 100644 frontend/src/views/Flow/WorkflowList.vue create mode 100644 frontend/src/views/Knowledge.vue create mode 100644 frontend/src/views/Login.vue create mode 100644 frontend/src/views/MCPServiceManagement.vue create mode 100644 frontend/src/views/Profile.vue create mode 100644 frontend/src/views/Register.vue create mode 100644 frontend/src/views/SystemManagement.vue create mode 100644 frontend/tsconfig.json create mode 100644 frontend/vite.config.ts diff --git a/README.md b/README.md new file mode 100644 index 0000000..fc5d3d8 --- /dev/null +++ b/README.md @@ -0,0 +1,464 @@ +# th - 企业级智能体应用平台 + + +🚀 **完全开源的大模型应用平台** +- 集成智能问答、智能问数、知识库、工作流和智能体编排的大模型解决方案。 +- 采用Vue.js + FastAPI + PostgreSQL+Langchain/LangGraph架构。 +- 专为企业级应用设计,代码完全开源,支持私有化部署,可灵活扩展及二次开发。 +- 用户级数据隔离:每个用户的数据仅对其可见,确保数据安全。 + + + +## 🏗️ 技术架构 + + + +### 后端技术栈 +- **Web框架**: FastAPI + SQLAlchemy + Alembic +- **数据库**: PostgreSQL 16+ (开源关系型数据库) +- **向量数据库**: PostgreSQL + pgvector 扩展 (开源向量数据库) +- **智能体编排**: LangGraph 状态图 + 条件路由 +- **工具调用**: Function Calling +- **模型连接协议**: MCP (Model Context Protocol) +- **RAG检索**: LangChain Vector Store +- **对话记忆**: ConversationBufferMemory +- **文档处理**: PyPDF2 + python-docx + markdown +- **数据分析**: Pandas + NumPy + +### 前端技术栈 +- **框架**: Vue 3 + TypeScript + Vite +- **UI组件**: Element Plus (开源UI库) +- **HTTP客户端**: Axios +- **工作流编辑器**: 自研可视化编辑器 +- **工作流引擎**: 基于DAG的流程执行引擎 +- **图形渲染**: Canvas API + SVG +- **拖拽交互**: Vue Draggable +- **节点连接**: 自定义连线算法 + + + +## 本地部署指南 + + +### 环境要求 +- Python 3.10+ +- Node.js 18+ +- PostgreSQL 16+ + +### 1. 安装数据库:PostgreSQL及pgvector插件(向量搜索) + +#### 方式一:Docker安装(推荐) +使用 Docker + Docker Compose 部署 PostgreSQL 16 + pgvector 插件。 + +**1. 创建docker-compose.yml文件** + +内容如下: + +```yaml +version: '3.8' + +services: + db: + image: pgvector/pgvector:pg16 + container_name: pgvector-db + environment: + POSTGRES_USER: myuser + POSTGRES_PASSWORD: your_password + POSTGRES_DB: mydb + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + restart: unless-stopped + +volumes: + pgdata: +``` + +**说明:** +- 使用 `pgvector/pgvector:pg16` 镜像,内置 PostgreSQL 16 + pgvector 插件 +- 数据保存在 Docker 卷 `pgdata` 中,重启不会丢失 +- 监听宿主机端口 5432,可用本地工具如 pgAdmin, DBeaver, psql 连接 +- 默认数据库名称:mydb +- 默认用户名:myuser +- 默认密码:your_password + +**2. 启动服务** + +在 `docker-compose.yml` 所在目录下运行: +```bash +docker-compose up -d +``` + +查看容器状态: +```bash +docker ps +``` + +输出应包含一个名为 `pgvector-db` 的容器,状态为 Up。 + +**3. 验证 pgvector 安装成功** + +进入 PostgreSQL 容器: +```bash +docker exec -it pgvector-db psql -U myuser -d mydb +``` + +启用 pgvector 插件: +```sql +CREATE EXTENSION IF NOT EXISTS vector; +``` + +插入并查询向量数据(示例,可以在客户端,如dbeaver等)** + +```sql +-- 创建表,包含一个向量字段(维度为3) +CREATE TABLE items ( + id SERIAL PRIMARY KEY, + embedding vector(3) +); + +-- 插入向量数据 +INSERT INTO items (embedding) VALUES + ('[1,1,1]'), + ('[2,2,2]'), + ('[1,0,0]'); + +-- 查询与 [1,1,1] 最接近的向量(基于欧几里得距离) +SELECT id, embedding +FROM items +ORDER BY embedding <-> '[1,1,1]' +LIMIT 3; +``` +-- 上述没报错且有结果返回,即安装成功 + +### 2. 后端部署 +```bash +# 克隆项目 +git clone https://github.com/lkpAgent/chat-agent.git +cd chat-agent/backend + +#创建python虚拟环境,推荐使用conda创建虚拟环境 +conda create -n chat-agent python=3.10 +conda activate chat-agent + +# 安装依赖 +pip install -r requirements.txt + +# 配置环境变量,windows下直接复制.env.example文件为.env +cp .env.example .env + +# 编辑.env文件,配置数据库连接和AI API密钥。相关配置信息见后面的配置说明 + +# 配置完数据库信息后,初始化数据库表及创建登录账号(用户名: test@example.com, 密码: 123456) +cd backend/tests +python init_db.py + +# 启动后端服务,默认8000端口 +python -m uvicorn th_agenter.main:app --reload --host 0.0.0.0 --port 8000 +# 或者直接运行main.py +# cd backend/th_agenter +# python main.py + +``` + + +### 3. 前端部署 +```bash +# 进入前端目录 +cd ../frontend + +# 安装依赖 +npm install + +# 配置环境变量 +cp .env.example .env +# 编辑.env文件,配置后端API地址 +VITE_API_BASE_URL = http://localhost:8000 + +# 开发环境,启动前端服务,默认端口3000 +npm run dev +# 发布到生产环境,比如部署在{nginx_home}/html/yourdomain,则指定base路径编译 +# npm run build -- --base=yourdomain +``` +启动成功后,访问http://localhost:3000,会进入到登录页面,默认账号密码为test@example.com/123456 + +![登录界面](docs/images/login.png) + +### 4. 访问应用 +- 前端地址: http://localhost:3000 +- 后端API: http://localhost:8000 +- API文档: http://localhost:8000/docs + +### 5. 后端配置说明 + +#### 后端环境变量配置 (backend/.env) +几个核心配置:系统数据库地址DATABASE_URL,向量数据库配置,CHAT大模型提供商:LLM_PROVIDER及相关配置,向量大模型提供商:EMBEDDING_PROVIDER +几个工具API_KEY:tavilySearch,心知天气API + +```env + +# 数据库配置 +# ======================================== +DATABASE_URL=postgresql://your_username:your_password@your_host:your_port/your_db +# 示例: +# DATABASE_URL=postgresql://myuser:mypassword@127.0.0.1:5432/mydb + +# ======================================== +# 向量数据库配置 +# ======================================== +VECTOR_DB_TYPE=pgvector +PGVECTOR_HOST=your_host +PGVECTOR_PORT=your_port +PGVECTOR_DATABASE=mydb +PGVECTOR_USER=myuser +PGVECTOR_PASSWORD=your_password + +# 大模型配置 (支持OpenAI协议的第三方服务) 只需要配置一种chat大模型以及embedding大模型 +# ======================================== +# chat大模型配置 +# ======================================== +# 可选择的提供商: openai, deepseek, doubao, zhipu, moonshot +LLM_PROVIDER=doubao + +# Embedding模型配置 +# ======================================== +# 可选择的提供商: openai, deepseek, doubao, zhipu, moonshot +EMBEDDING_PROVIDER=zhipu + +# OpenAI配置 +OPENAI_API_KEY=your-openai-api-key +OPENAI_MODEL=gpt-4 +OPENAI_BASE_URL=https://api.openai.com/v1 +OPENAI_EMBEDDING_MODEL=text-embedding-ada-002 + + +# 智谱AI配置 +ZHIPU_API_KEY=your-zhipu-api-key +ZHIPU_MODEL=glm-4 +ZHIPU_EMBEDDING_MODEL=embedding-3 +ZHIPU_BASE_URL=https://open.bigmodel.cn/api/paas/v4 + +# DeepSeek配置 +DEEPSEEK_API_KEY=your-deepseek-api-key +DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 +DEEPSEEK_MODEL=deepseek-chat +DEEPSEEK_EMBEDDING_MODEL=deepseek-embedding + +# 豆包配置 +DOUBAO_API_KEY=your-doubao-api-key +DOUBAO_BASE_URL=https://ark.cn-beijing.volces.com/api/v3 +DOUBAO_MODEL=doubao-1-5-pro-32k-250115 +DOUBAO_EMBEDDING_MODEL=doubao-embedding + +# Moonshot配置 +MOONSHOT_API_KEY=your-moonshot-api-key +MOONSHOT_BASE_URL=https://api.moonshot.cn/v1 +MOONSHOT_MODEL=moonshot-v1-8k +MOONSHOT_EMBEDDING_MODEL=moonshot-embedding + +# 工具API配置 +## tavilySearch api +TAVILY_API_KEY=your-tavily-api-key +## 心知天气api +WEATHER_API_KEY=your_xinzhi_api_key +``` + + +## 📖 API文档 + +### 主要API端点 + +#### 认证相关 +- `POST /auth/login` - 用户登录 +- `POST /auth/register` - 用户注册 +- `POST /auth/refresh` - 刷新Token + +#### 对话管理 +- `GET /chat/conversations` - 获取对话列表 +- `POST /chat/conversations` - 创建新对话 +- `POST /chat/conversations/{id}/chat` - 发送消息 + +#### 知识库管理 +- `POST /knowledge/upload` - 上传文档 +- `GET /knowledge/documents` - 获取文档列表 +- `DELETE /knowledge/documents/{id}` - 删除文档 + +#### 智能查询 +- `POST /smart-query/query` - 智能数据查询 +- `POST /smart-query/upload` - 上传Excel文件 +- `GET /smart-query/files` - 获取文件列表 + +### 完整API文档 +启动后端服务后访问: http://localhost:8000/docs + +## 🔧 开发指南 + +### 项目结构 +``` +open-agent/ +├── backend/ # 后端代码 +│ ├── th_agenter/ # 主应用包 +│ │ ├── api/ # API路由 +│ │ ├── core/ # 核心配置 +│ │ ├── db/ # 数据库相关 +│ │ ├── models/ # 数据库模型 +│ │ ├── services/ # 业务逻辑 +│ │ ├── utils/ # 工具函数 +│ │ └── main.py # 应用入口 +│ ├── tests/ # 测试文件 +│ └── requirements.txt # Python依赖 +├── frontend/ # 前端代码 +│ ├── src/ +│ │ ├── components/ # Vue组件 +│ │ ├── views/ # 页面组件 +│ │ │ ├── chat/ # 对话页面 +│ │ │ ├── knowledge/ # 知识库页面 +│ │ │ ├── workflow/ # 工作流页面 +│ │ │ └── agent/ # 智能体页面 +│ │ ├── stores/ # Pinia状态管理 +│ │ ├── api/ # API调用 +│ │ ├── types/ # TypeScript类型 +│ │ └── router/ # 路由配置 +│ └── package.json # Node.js依赖 +├── data/ # 数据目录 +│ ├── uploads/ # 上传文件 +│ └── logs/ # 日志文件 +└── docs/ # 文档目录 +``` + + +## ✨ 核心能力 + + + +### 🤖 智能问答 + + +- **多模型支持**:集成DeepSeek、智谱AI、豆包等国内主流AI服务商 +- **三种对话模式**: + - 自由对话:直接与AI模型交互 + - RAG对话:基于知识库的检索增强生成 + - 智能体对话:多智能体协作处理复杂任务 +- **多轮对话**:支持连续对话,上下文理解和记忆 +- **对话历史**:完整的会话记录和管理 + +## 🌟 技术特色 + +### 基于LangGraph的智能体对话系统 +- **自主规划能力**:智能体能够根据任务需求自主调用工具并规划执行流程 +- **动态工具调用**:根据上下文自动选择最合适的工具并执行 +- **多步任务分解**:复杂任务自动拆解为多个子任务并顺序执行 + +**示例场景**: +当用户询问"推荐长沙和北京哪个适宜旅游"时: +1. 智能体首先调用搜索工具查找相关信息 +2. 未找到合适结果时,自动规划调用天气查询工具 +3. 智能拆分为两次执行:先查询长沙天气,再查询北京天气 +4. 根据气温数据判断北京更适宜旅游 +5. 自动调用搜索工具查找北京景点信息 +6. 最终整合所有信息生成总结推荐 + +第一步:调用搜索引擎搜索哪个城市更适宜旅游 +![智能体问答界面](docs/images/agent1.png) + +第二步:搜索的内容没有找到合适的答案,意识到错了后,改变策划,重新调用天气工具,从天气的角度判断哪个城市更适合当下旅游 。 +并且自动进行任务拆解,对北京、长沙分别调用一次天气工具,获取到两个城市的天气情况。 + +![智能体问答界面](docs/images/agent2.png) + +第三步:根据天气判断北京更适合旅游,再调用搜索引擎工具,搜索北京的特色景点。最后将工具调用结果与问题进行总结,完成本次对话过程。 + +![智能体问答界面](docs/images/agent3.png) + +### 📊 智能问数 + + +- **Excel分析**:上传Excel文件进行智能数据分析 +- **自然语言查询**:用自然语言提问,自动生成Python代码 +- **数据库查询**:连接PostgreSQL等数据库进行智能问答 +- **多表关联**:支持复杂的多表/多文件联合查询 +- **可视化思维链**:大模型思考过程可视化呈现 + +## 🌟 技术特色 + +### 双引擎智能问数系统 + +**基于Excel的智能问数** +- 使用LangChain代码解释器插件,将Excel数据读取到Pandas DataFrame +- 大模型将自然语言问题转换为Pandas语法并执行 +- 支持多表格文件联合查询和复杂数据分析 + +**基于数据库的智能问数** +- 实现PostgreSQL MCP(Model Context Protocol)接口 +- 大模型先提取表元数据,了解表结构和关系 +- 根据用户问题自动生成优化SQL查询语句 +- 支持多表关联查询和复杂数据检索 + +基于Excel报表的智能问数 +![智能问数界面](docs/images/smart_data.png) +基于数据库的智能问数 +![智能问数界面](docs/images/smart_data_db.png) +### 📚 知识库管理 + + + +- **文档处理**:支持PDF、Word、Markdown、TXT等格式 +- **向量存储**:基于PostgreSQL + pgvector的向量数据库 +- **智能检索**:向量相似度搜索和BM25算法关键词检索 +- **文档管理**:上传、删除、分类和标签管理 +- **RAG集成**:与对话系统无缝集成 + +## 🌟 技术特色 +### 高级语义分割知识库处理 +- **智能段落分割**:基于大模型的语义理解分割技术,而非传统的文本相似度判断 +- **精准切分识别**:大模型直接识别最适合的切分位置并输出分割标记字符串 +- **高效处理流程**:仅输出分割位置字符串,再由代码执行实际分割操作 +- **性能优化**:避免了传统方法中大量的向量计算和相似度比较,提升处理速度 +- **质量保证**:大模型的深层语义理解确保分割边界的准确性和合理性 +### 双重召回检索机制 +- **多模态检索**:结合向量相似度匹配(语义搜索)与BM25关键词检索(字面匹配) +- **混合排序策略**:采用加权融合算法,综合语义相关性和关键词匹配度进行结果排序 +- **召回增强**:双重召回机制有效解决了单纯向量检索的"词汇不匹配"问题 +- **精准度提升**:相比单一检索方式,显著提高相关文档的召回率和准确率 + +![知识库管理界面](docs/images/knowledge_base.png) +![知识库管理界面](docs/images/split.png) + + +### 🔧 工作流编排 + +- **可视化设计**:拖拽式工作流设计器 +- **节点类型**:支持AI对话、数据处理、条件判断等节点 +- **流程控制**:条件分支、循环、并行执行 + +![工作流编排界面](docs/images/workflow.png) + +### 🤖 智能体编排 +- **多智能体协作**:不同专业领域的AI智能体协同工作 +- **角色定义**:自定义智能体的专业能力和知识领域 +- **任务分配**:智能分解复杂任务到合适的智能体 +- **结果整合**:汇总多个智能体的输出生成最终答案 +### 在线体验地址,可自己注册账号使用 +http://113.240.110.92:81/ + +#### 💼 商业使用 +- ✅ 可用于商业项目 +- ✅ 可修改源码 +- ✅ 可私有化部署 +- ✅ 可集成到现有系统 +- ✅ 无需支付许可费用 + +## 📄 许可证 + +本项目采用 [MIT License](LICENSE) 许可证,这意味着: +- 可以自由使用、修改、分发 +- 可以用于商业目的 +- 只需保留原始许可证声明 +- 作者不承担任何责任 + +## 🙏 致谢 + + + +**如果这个项目对你有帮助,请给它一个 ⭐️!** \ No newline at end of file diff --git a/backend/.env b/backend/.env new file mode 100644 index 0000000..4af5cae --- /dev/null +++ b/backend/.env @@ -0,0 +1,90 @@ +# ======================================== +# 大模型配置 (支持OpenAI协议的第三方服务) +# ======================================== +# 可选择的提供商: openai, deepseek, doubao, zhipu, moonshot +LLM_PROVIDER=doubao + +# Embedding模型配置 +# 可选择的提供商: openai, deepseek, doubao, zhipu, moonshot +EMBEDDING_PROVIDER=zhipu + +# OpenAI配置 +OPENAI_API_KEY=your-openai-api-key +OPENAI_BASE_URL=https://api.openai.com/v1 +OPENAI_MODEL=gpt-3.5-turbo + +# DeepSeek配置 +DEEPSEEK_API_KEY=your-deepseek-api-key +DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 +DEEPSEEK_MODEL=deepseek-chat + +# 豆包(字节跳动)配置 +DOUBAO_API_KEY=your-doubao-api-key +DOUBAO_BASE_URL=https://ark.cn-beijing.volces.com/api/v3 +DOUBAO_MODEL=doubao-1-5-pro-32k-250115 + +# 智谱AI配置 +ZHIPU_API_KEY=your-zhipu-api-key +ZHIPU_BASE_URL=https://open.bigmodel.cn/api/paas/v4 +ZHIPU_MODEL=glm-4 +ZHIPU_EMBEDDING_MODEL=embedding-3 + +# 月之暗面配置 +MOONSHOT_API_KEY=your-moonshot-api-key +MOONSHOT_BASE_URL=https://api.moonshot.cn/v1 +MOONSHOT_MODEL=moonshot-v1-8k +MOONSHOT_EMBEDDING_MODEL=moonshot-embedding + +# Embedding模型配置 +OPENAI_EMBEDDING_MODEL=text-embedding-ada-002 +DEEPSEEK_EMBEDDING_MODEL=deepseek-embedding +DOUBAO_EMBEDDING_MODEL=doubao-embedding + +# 工具API配置 +## tavilySearch api +TAVILY_API_KEY=your-tavily-api-key +## 心知天气api +WEATHER_API_KEY=your_xinzhi_api_key + +# ======================================== +# 应用配置 +# ======================================== +# 后端应用配置 +APP_NAME=TH-Agenter +APP_VERSION=0.1.0 +DEBUG=true +ENVIRONMENT=development +HOST=0.0.0.0 +PORT=8000 + +# 前端应用配置 +VITE_API_BASE_URL=http://localhost:8000/api +VITE_APP_TITLE=TH-Agenter +VITE_APP_VERSION=1.0.0 +VITE_ENABLE_MOCK=false +VITE_UPLOAD_MAX_SIZE=10485760 +VITE_SUPPORTED_FILE_TYPES=pdf,txt,md,doc,docx,ppt,pptx,xls,xlsx + +# ======================================== +# 安全配置 +# ======================================== +SECRET_KEY=your-secret-key-here-change-in-production +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=300 + +# ======================================== +# 数据库配置 +# ======================================== +# 数据库URL配置 +DATABASE_URL=sqlite:///./TH-Agenter.db +# DATABASE_URL=postgresql://iagent:iagent@118.196.30.45:5432/iagent + +# ======================================== +# 向量数据库配置 +# ======================================== +VECTOR_DB_TYPE=pgvector +PGVECTOR_HOST=118.196.30.45 +PGVECTOR_PORT=5432 +PGVECTOR_DATABASE=iagent +PGVECTOR_USER=iagent +PGVECTOR_PASSWORD=iagent \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..f213015 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,89 @@ +# ======================================== +# 大模型配置 (支持OpenAI协议的第三方服务) +# ======================================== +# 可选择的提供商: openai, deepseek, doubao, zhipu, moonshot +LLM_PROVIDER=doubao + +# Embedding模型配置 +# 可选择的提供商: openai, deepseek, doubao, zhipu, moonshot +EMBEDDING_PROVIDER=zhipu + +# OpenAI配置 +OPENAI_API_KEY=your-openai-api-key +OPENAI_BASE_URL=https://api.openai.com/v1 +OPENAI_MODEL=gpt-3.5-turbo + +# DeepSeek配置 +DEEPSEEK_API_KEY=your-deepseek-api-key +DEEPSEEK_BASE_URL=https://api.deepseek.com/v1 +DEEPSEEK_MODEL=deepseek-chat + +# 豆包(字节跳动)配置 +DOUBAO_API_KEY=your-doubao-api-key +DOUBAO_BASE_URL=https://ark.cn-beijing.volces.com/api/v3 +DOUBAO_MODEL=doubao-1-5-pro-32k-250115 + +# 智谱AI配置 +ZHIPU_API_KEY=your-zhipu-api-key +ZHIPU_BASE_URL=https://open.bigmodel.cn/api/paas/v4 +ZHIPU_MODEL=glm-4 +ZHIPU_EMBEDDING_MODEL=embedding-3 + +# 月之暗面配置 +MOONSHOT_API_KEY=your-moonshot-api-key +MOONSHOT_BASE_URL=https://api.moonshot.cn/v1 +MOONSHOT_MODEL=moonshot-v1-8k +MOONSHOT_EMBEDDING_MODEL=moonshot-embedding + +# Embedding模型配置 +OPENAI_EMBEDDING_MODEL=text-embedding-ada-002 +DEEPSEEK_EMBEDDING_MODEL=deepseek-embedding +DOUBAO_EMBEDDING_MODEL=doubao-embedding + +# 工具API配置 +## tavilySearch api +TAVILY_API_KEY=your-tavily-api-key +## 心知天气api +WEATHER_API_KEY=your_xinzhi_api_key + +# ======================================== +# 应用配置 +# ======================================== +# 后端应用配置 +APP_NAME=TH-Agenter +APP_VERSION=0.1.0 +DEBUG=true +ENVIRONMENT=development +HOST=0.0.0.0 +PORT=8000 + +# 前端应用配置 +VITE_API_BASE_URL=http://localhost:8000/api +VITE_APP_TITLE=TH-Agenter +VITE_APP_VERSION=1.0.0 +VITE_ENABLE_MOCK=false +VITE_UPLOAD_MAX_SIZE=10485760 +VITE_SUPPORTED_FILE_TYPES=pdf,txt,md,doc,docx,ppt,pptx,xls,xlsx + +# ======================================== +# 安全配置 +# ======================================== +SECRET_KEY=your-secret-key-here-change-in-production +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=300 + +# ======================================== +# 数据库配置 +# ======================================== + +DATABASE_URL=postgresql://iagent:iagent@118.196.30.45:5432/iagent + +# ======================================== +# 向量数据库配置 +# ======================================== +VECTOR_DB_TYPE=pgvector +PGVECTOR_HOST=localhost +PGVECTOR_PORT=5432 +PGVECTOR_DATABASE=mydb +PGVECTOR_USER=myuser +PGVECTOR_PASSWORD=mypassword \ No newline at end of file diff --git a/backend/TH-Agenter.db b/backend/TH-Agenter.db new file mode 100644 index 0000000000000000000000000000000000000000..15f889c2f5fac0317392036f18d4bf7842997b26 GIT binary patch literal 376832 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 B+*AMn literal 0 HcmV?d00001 diff --git a/backend/configs/settings.yaml b/backend/configs/settings.yaml new file mode 100644 index 0000000..d2afc03 --- /dev/null +++ b/backend/configs/settings.yaml @@ -0,0 +1,49 @@ +# Chat Agent Configuration +app: + name: "TH-Agenter" + version: "0.1.0" + debug: true + environment: "development" + host: "0.0.0.0" + port: 8000 + +# File Configuration +file: + upload_dir: "./data/uploads" + max_size: 10485760 # 10MB + allowed_extensions: [".txt", ".pdf", ".docx", ".md"] + chunk_size: 1000 + chunk_overlap: 200 + semantic_splitter_enabled: true # 启用语义分割器 + +# Storage Configuration +storage: + storage_type: "local" # local or s3 + upload_directory: "./data/uploads" + + # S3 Configuration + s3_bucket_name: "chat-agent-files" + aws_access_key_id: null + aws_secret_access_key: null + aws_region: "us-east-1" + s3_endpoint_url: null + +# Logging Configuration +logging: + level: "INFO" + file: "./data/logs/app.log" + format: "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + max_bytes: 10485760 # 10MB + backup_count: 5 + +# CORS Configuration +cors: + allowed_origins: ["*"] + allowed_methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"] + allowed_headers: ["*"] + +# Chat Configuration +chat: + max_history_length: 10 + system_prompt: "你是一个有用的AI助手,请根据提供的上下文信息回答用户的问题。" + max_response_tokens: 1000 \ No newline at end of file diff --git a/backend/data/logs/app.log b/backend/data/logs/app.log new file mode 100644 index 0000000..f386176 --- /dev/null +++ b/backend/data/logs/app.log @@ -0,0 +1,3668 @@ +2025-12-04 11:02:51,315 - root - INFO - Logging configured successfully +2025-12-04 11:02:51,440 - root - INFO - Logging configured successfully +2025-12-04 11:02:51,630 - root - INFO - Starting up TH-Agenter application... +2025-12-04 11:02:51,660 - root - INFO - SQLite database engine created: sqlite:///./TH-Agenter.db +2025-12-04 11:02:51,799 - root - INFO - Database tables created +2025-12-04 11:02:51,799 - root - INFO - Database initialized +2025-12-04 13:10:14,338 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:14,340 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:14,341 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:14,342 - root - INFO - Clearing user context +2025-12-04 13:10:14,805 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:14,805 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:14,805 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:14,806 - root - INFO - Clearing user context +2025-12-04 13:10:15,152 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:15,152 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:15,152 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:15,153 - root - INFO - Clearing user context +2025-12-04 13:10:16,868 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:16,868 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:16,869 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:16,869 - root - INFO - Clearing user context +2025-12-04 13:10:17,206 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:17,207 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:17,207 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:17,207 - root - INFO - Clearing user context +2025-12-04 13:10:17,343 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:17,343 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:17,344 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:17,344 - root - INFO - Clearing user context +2025-12-04 13:10:17,501 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:17,502 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:17,502 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:17,502 - root - INFO - Clearing user context +2025-12-04 13:10:18,639 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:18,639 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:18,640 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:18,640 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:18,653 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:10:19,955 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:19,956 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:19,956 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:19,956 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:19,961 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:10:20,489 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:20,490 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:20,490 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:20,490 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:20,493 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:10:20,696 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:20,696 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:20,696 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:20,696 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:20,699 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:10:20,871 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:20,872 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:20,872 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:20,872 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:20,875 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:10:21,071 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:21,071 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:21,072 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:21,072 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:21,075 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:10:21,263 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:21,263 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:21,263 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:21,264 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:21,267 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:10:24,880 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:24,880 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:24,880 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:24,881 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:24,884 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:10:28,598 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:28,599 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:28,599 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:28,599 - root - INFO - Clearing user context +2025-12-04 13:10:28,625 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:28,626 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:28,626 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:28,627 - root - INFO - Clearing user context +2025-12-04 13:10:28,833 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:28,834 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:28,834 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:28,834 - root - INFO - Clearing user context +2025-12-04 13:10:28,840 - root - INFO - [MIDDLEWARE] Processing request: GET /api/knowledge-bases/ +2025-12-04 13:10:28,841 - root - INFO - [MIDDLEWARE] Checking path: /api/knowledge-bases/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:28,841 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/knowledge-bases/ +2025-12-04 13:10:28,841 - root - INFO - Clearing user context +2025-12-04 13:10:28,844 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:10:28,845 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:28,845 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:10:28,845 - root - INFO - Clearing user context +2025-12-04 13:10:28,852 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:10:28,853 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:28,853 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:10:28,853 - root - INFO - Clearing user context +2025-12-04 13:10:28,857 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:10:28,858 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:28,858 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:10:28,859 - root - INFO - Clearing user context +2025-12-04 13:10:28,862 - root - INFO - [MIDDLEWARE] Processing request: GET /api/knowledge-bases/ +2025-12-04 13:10:28,863 - root - INFO - [MIDDLEWARE] Checking path: /api/knowledge-bases/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:28,863 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/knowledge-bases/ +2025-12-04 13:10:28,864 - root - INFO - Clearing user context +2025-12-04 13:10:28,870 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:10:28,870 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:28,870 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:10:28,871 - root - INFO - Clearing user context +2025-12-04 13:10:31,010 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:31,010 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:31,011 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:31,011 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:31,013 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:10:58,228 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:10:58,228 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:10:58,229 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:10:58,229 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:10:58,232 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:11:12,492 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:11:12,493 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:11:12,493 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:11:12,493 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:11:12,496 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:11:13,239 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:11:13,239 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:11:13,240 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:11:13,240 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:11:13,243 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:11:13,449 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:11:13,450 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:11:13,450 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:11:13,450 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:11:13,454 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:11:13,657 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:11:13,657 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:11:13,657 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:11:13,658 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:11:13,661 - root - ERROR - Database session error: 401: Incorrect email or password +2025-12-04 13:13:12,453 - root - INFO - Shutting down TH-Agenter application... +2025-12-04 13:13:19,714 - root - INFO - Logging configured successfully +2025-12-04 13:13:19,817 - root - INFO - Logging configured successfully +2025-12-04 13:13:20,007 - root - INFO - Starting up TH-Agenter application... +2025-12-04 13:13:20,037 - root - INFO - SQLite database engine created: sqlite:///./TH-Agenter.db +2025-12-04 13:13:20,043 - root - INFO - Database tables created +2025-12-04 13:13:20,043 - root - INFO - Database initialized +2025-12-04 13:14:21,552 - root - INFO - Shutting down TH-Agenter application... +2025-12-04 13:14:42,425 - root - INFO - Logging configured successfully +2025-12-04 13:14:42,531 - root - INFO - SQLite database engine created: sqlite:///./TH-Agenter.db +2025-12-04 13:14:42,535 - root - INFO - Database tables created +2025-12-04 13:14:42,790 - th_agenter.th_agenter.services.user - INFO - User created successfully: test +2025-12-04 13:15:01,086 - root - INFO - Logging configured successfully +2025-12-04 13:15:01,183 - root - INFO - Logging configured successfully +2025-12-04 13:15:01,329 - root - INFO - Starting up TH-Agenter application... +2025-12-04 13:15:01,347 - root - INFO - SQLite database engine created: sqlite:///./TH-Agenter.db +2025-12-04 13:15:01,352 - root - INFO - Database tables created +2025-12-04 13:15:01,352 - root - INFO - Database initialized +2025-12-04 13:19:26,858 - root - INFO - Shutting down TH-Agenter application... +2025-12-04 13:20:43,291 - root - INFO - Logging configured successfully +2025-12-04 13:20:43,383 - root - INFO - Logging configured successfully +2025-12-04 13:20:43,531 - root - INFO - Starting up TH-Agenter application... +2025-12-04 13:20:43,548 - root - INFO - SQLite database engine created: sqlite:///./TH-Agenter.db +2025-12-04 13:20:43,552 - root - INFO - Database tables created +2025-12-04 13:20:43,552 - root - INFO - Database initialized +2025-12-04 13:20:54,188 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:20:54,188 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:54,189 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:20:54,189 - root - INFO - Clearing user context +2025-12-04 13:20:54,243 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:54,243 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:54,243 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:54,244 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:54,247 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:20:54,247 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764828763} +2025-12-04 13:20:54,248 - root - INFO - Looking for user with username: admin +2025-12-04 13:20:54,250 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:20:54,250 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:54,250 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:20:54,258 - root - INFO - Clearing user context +2025-12-04 13:20:54,269 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/roles/user-roles/user/2 +2025-12-04 13:20:54,270 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/roles/user-roles/user/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:54,270 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/roles/user-roles/user/2 +2025-12-04 13:20:54,270 - root - INFO - Clearing user context +2025-12-04 13:20:54,273 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:54,273 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:54,273 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:54,274 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:54,275 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:20:54,276 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764828763} +2025-12-04 13:20:54,276 - root - INFO - Looking for user with username: admin +2025-12-04 13:20:54,277 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:20:54,278 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:54,278 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:20:54,283 - root - INFO - Clearing user context +2025-12-04 13:20:55,495 - root - INFO - [MIDDLEWARE] Processing request: POST /api/auth/login +2025-12-04 13:20:55,496 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/login against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:55,496 - root - INFO - [MIDDLEWARE] Path /api/auth/login exactly matches exclude_path /api/auth/login +2025-12-04 13:20:55,496 - root - INFO - [MIDDLEWARE] Skipping authentication for excluded path: /api/auth/login +2025-12-04 13:20:55,734 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:20:55,735 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:55,735 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:20:55,735 - root - INFO - Clearing user context +2025-12-04 13:20:55,737 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:55,737 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,738 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:55,738 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:55,740 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:20:55,740 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:20:55,740 - root - INFO - Looking for user with username: admin +2025-12-04 13:20:55,742 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:20:55,742 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,742 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:20:55,744 - root - INFO - Clearing user context +2025-12-04 13:20:55,751 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/roles/user-roles/user/2 +2025-12-04 13:20:55,752 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/roles/user-roles/user/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:55,752 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/roles/user-roles/user/2 +2025-12-04 13:20:55,752 - root - INFO - Clearing user context +2025-12-04 13:20:55,754 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:55,755 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,755 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:55,755 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:55,757 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:20:55,757 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:20:55,757 - root - INFO - Looking for user with username: admin +2025-12-04 13:20:55,759 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:20:55,759 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,760 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:20:55,762 - root - INFO - Clearing user context +2025-12-04 13:20:55,904 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:20:55,905 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:55,905 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:20:55,905 - root - INFO - Clearing user context +2025-12-04 13:20:55,908 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:55,908 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,908 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:55,909 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:55,910 - root - INFO - [MIDDLEWARE] Processing request: GET /api/knowledge-bases/ +2025-12-04 13:20:55,911 - root - INFO - [MIDDLEWARE] Checking path: /api/knowledge-bases/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:55,911 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/knowledge-bases/ +2025-12-04 13:20:55,911 - root - INFO - Clearing user context +2025-12-04 13:20:55,913 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:55,914 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,914 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:55,914 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:55,930 - root - INFO - Clearing user context +2025-12-04 13:20:55,932 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:20:55,932 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:20:55,933 - root - INFO - Looking for user with username: admin +2025-12-04 13:20:55,935 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:20:55,935 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,935 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:20:55,960 - root - INFO - Clearing user context +2025-12-04 13:20:55,962 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:20:55,962 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:55,963 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:20:55,963 - root - INFO - Clearing user context +2025-12-04 13:20:55,966 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:55,967 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,967 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:55,967 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:55,969 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 13:20:55,970 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:55,970 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 13:20:55,970 - root - INFO - Clearing user context +2025-12-04 13:20:55,972 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:55,973 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,973 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:55,973 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:55,980 - root - INFO - [MIDDLEWARE] Processing request: GET /api/knowledge-bases/ +2025-12-04 13:20:55,981 - root - INFO - [MIDDLEWARE] Checking path: /api/knowledge-bases/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:55,981 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/knowledge-bases/ +2025-12-04 13:20:55,981 - root - INFO - Clearing user context +2025-12-04 13:20:55,983 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:55,983 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,983 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:55,984 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:55,986 - root - INFO - Clearing user context +2025-12-04 13:20:55,988 - root - INFO - Clearing user context +2025-12-04 13:20:55,992 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:20:55,993 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:20:55,994 - root - INFO - Looking for user with username: admin +2025-12-04 13:20:55,996 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:20:55,996 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:55,997 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:20:55,997 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:20:55,997 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:55,998 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:20:55,998 - root - INFO - Clearing user context +2025-12-04 13:20:56,000 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:56,000 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:56,000 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:56,000 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:56,008 - root - INFO - Clearing user context +2025-12-04 13:20:56,009 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 13:20:56,010 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:56,010 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 13:20:56,010 - root - INFO - Clearing user context +2025-12-04 13:20:56,012 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:56,012 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:56,013 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:56,013 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:56,018 - root - INFO - Clearing user context +2025-12-04 13:20:56,024 - root - INFO - Clearing user context +2025-12-04 13:20:56,034 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 13:20:56,034 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:20:56,034 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 13:20:56,035 - root - INFO - Clearing user context +2025-12-04 13:20:56,036 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:20:56,036 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:20:56,037 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:20:56,037 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:20:56,039 - root - INFO - Clearing user context +2025-12-04 13:21:04,714 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/45 +2025-12-04 13:21:04,715 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/45 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:04,716 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/45 +2025-12-04 13:21:04,716 - root - INFO - Clearing user context +2025-12-04 13:21:04,722 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:04,722 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:04,722 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:04,722 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:04,729 - root - INFO - Clearing user context +2025-12-04 13:21:04,744 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/45/messages +2025-12-04 13:21:04,744 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/45/messages against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:04,745 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/45/messages +2025-12-04 13:21:04,745 - root - INFO - Clearing user context +2025-12-04 13:21:04,747 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:04,747 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:04,747 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:04,747 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:04,756 - root - INFO - Clearing user context +2025-12-04 13:21:05,324 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/44 +2025-12-04 13:21:05,325 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/44 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:05,325 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/44 +2025-12-04 13:21:05,325 - root - INFO - Clearing user context +2025-12-04 13:21:05,326 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:05,327 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:05,327 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:05,327 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:05,330 - root - INFO - Clearing user context +2025-12-04 13:21:05,345 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/44/messages +2025-12-04 13:21:05,346 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/44/messages against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:05,346 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/44/messages +2025-12-04 13:21:05,346 - root - INFO - Clearing user context +2025-12-04 13:21:05,348 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:05,348 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:05,349 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:05,349 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:05,352 - root - INFO - Clearing user context +2025-12-04 13:21:05,734 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/43 +2025-12-04 13:21:05,734 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/43 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:05,735 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/43 +2025-12-04 13:21:05,735 - root - INFO - Clearing user context +2025-12-04 13:21:05,737 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:05,737 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:05,737 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:05,737 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:05,740 - root - INFO - Clearing user context +2025-12-04 13:21:05,753 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/43/messages +2025-12-04 13:21:05,754 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/43/messages against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:05,754 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/43/messages +2025-12-04 13:21:05,754 - root - INFO - Clearing user context +2025-12-04 13:21:05,755 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:05,756 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:05,756 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:05,756 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:05,758 - root - INFO - Clearing user context +2025-12-04 13:21:06,085 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/42 +2025-12-04 13:21:06,085 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/42 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:06,086 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/42 +2025-12-04 13:21:06,086 - root - INFO - Clearing user context +2025-12-04 13:21:06,088 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:06,089 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:06,089 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:06,089 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:06,092 - root - INFO - Clearing user context +2025-12-04 13:21:06,105 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/42/messages +2025-12-04 13:21:06,106 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/42/messages against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:06,106 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/42/messages +2025-12-04 13:21:06,106 - root - INFO - Clearing user context +2025-12-04 13:21:06,108 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:06,108 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:06,108 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:06,108 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:06,111 - root - INFO - Clearing user context +2025-12-04 13:21:06,388 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/40 +2025-12-04 13:21:06,389 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/40 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:06,389 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/40 +2025-12-04 13:21:06,389 - root - INFO - Clearing user context +2025-12-04 13:21:06,390 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:06,391 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:06,391 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:06,391 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:06,394 - root - INFO - Clearing user context +2025-12-04 13:21:06,406 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/40/messages +2025-12-04 13:21:06,406 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/40/messages against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:06,407 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/40/messages +2025-12-04 13:21:06,407 - root - INFO - Clearing user context +2025-12-04 13:21:06,408 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:06,409 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:06,409 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:06,409 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:06,412 - root - INFO - Clearing user context +2025-12-04 13:21:27,175 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:21:27,175 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,176 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:21:27,176 - root - INFO - Clearing user context +2025-12-04 13:21:27,178 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,178 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,179 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,179 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,181 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:27,182 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:27,182 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:27,184 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:27,184 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,185 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:27,188 - root - INFO - Clearing user context +2025-12-04 13:21:27,208 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/roles/user-roles/user/2 +2025-12-04 13:21:27,209 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/roles/user-roles/user/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,209 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/roles/user-roles/user/2 +2025-12-04 13:21:27,209 - root - INFO - Clearing user context +2025-12-04 13:21:27,211 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,212 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,212 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,213 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,215 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:27,215 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:27,216 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:27,218 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:27,218 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,218 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:27,224 - root - INFO - Clearing user context +2025-12-04 13:21:27,785 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:21:27,786 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,786 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:21:27,786 - root - INFO - Clearing user context +2025-12-04 13:21:27,788 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,788 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,789 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,789 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,790 - root - INFO - [MIDDLEWARE] Processing request: GET /api/knowledge-bases/ +2025-12-04 13:21:27,790 - root - INFO - [MIDDLEWARE] Checking path: /api/knowledge-bases/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,791 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/knowledge-bases/ +2025-12-04 13:21:27,791 - root - INFO - Clearing user context +2025-12-04 13:21:27,794 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,794 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,795 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,795 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,800 - root - INFO - Clearing user context +2025-12-04 13:21:27,801 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:27,801 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:27,802 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:27,803 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:27,804 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,804 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:27,812 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:21:27,812 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,812 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:21:27,813 - root - INFO - Clearing user context +2025-12-04 13:21:27,816 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,817 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,817 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,817 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,819 - root - INFO - Clearing user context +2025-12-04 13:21:27,821 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 13:21:27,821 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,821 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 13:21:27,821 - root - INFO - Clearing user context +2025-12-04 13:21:27,824 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,824 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,825 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,825 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,834 - root - INFO - Clearing user context +2025-12-04 13:21:27,838 - root - INFO - [MIDDLEWARE] Processing request: GET /api/knowledge-bases/ +2025-12-04 13:21:27,838 - root - INFO - [MIDDLEWARE] Checking path: /api/knowledge-bases/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,838 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/knowledge-bases/ +2025-12-04 13:21:27,839 - root - INFO - Clearing user context +2025-12-04 13:21:27,841 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,842 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,842 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,842 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,847 - root - INFO - Clearing user context +2025-12-04 13:21:27,849 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:21:27,850 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,850 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:21:27,850 - root - INFO - Clearing user context +2025-12-04 13:21:27,852 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,853 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,853 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,853 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,856 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:27,857 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:27,857 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:27,858 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 13:21:27,859 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:27,859 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,859 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,859 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 13:21:27,860 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:27,860 - root - INFO - Clearing user context +2025-12-04 13:21:27,862 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,862 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,862 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,863 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,869 - root - INFO - Clearing user context +2025-12-04 13:21:27,871 - root - INFO - Clearing user context +2025-12-04 13:21:27,877 - root - INFO - Clearing user context +2025-12-04 13:21:27,883 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 13:21:27,884 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:27,884 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 13:21:27,884 - root - INFO - Clearing user context +2025-12-04 13:21:27,886 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:27,886 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:27,887 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:27,887 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:27,890 - root - INFO - Clearing user context +2025-12-04 13:21:29,610 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 13:21:29,611 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:29,611 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 13:21:29,611 - root - INFO - Clearing user context +2025-12-04 13:21:29,613 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:29,614 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:29,614 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:29,614 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:29,617 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:29,617 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:29,618 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:29,619 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:29,620 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:29,620 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:29,636 - root - INFO - Clearing user context +2025-12-04 13:21:32,906 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 13:21:32,907 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:32,907 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 13:21:32,907 - root - INFO - Clearing user context +2025-12-04 13:21:32,909 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:32,909 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:32,909 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:32,910 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:32,912 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:32,912 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:32,913 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:32,916 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:32,916 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:32,916 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:32,919 - root - INFO - Clearing user context +2025-12-04 13:21:32,938 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/roles/user-roles/user/2 +2025-12-04 13:21:32,938 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/roles/user-roles/user/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:32,938 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/roles/user-roles/user/2 +2025-12-04 13:21:32,939 - root - INFO - Clearing user context +2025-12-04 13:21:32,940 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:32,941 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:32,941 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:32,941 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:32,943 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:32,944 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:32,944 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:32,946 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:32,946 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:32,946 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:32,950 - root - INFO - Clearing user context +2025-12-04 13:21:33,277 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 13:21:33,278 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:33,278 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 13:21:33,278 - root - INFO - Clearing user context +2025-12-04 13:21:33,280 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:33,280 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:33,281 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:33,281 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:33,283 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 13:21:33,283 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:33,284 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 13:21:33,284 - root - INFO - Clearing user context +2025-12-04 13:21:33,285 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:33,286 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:33,286 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:33,286 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:33,292 - root - INFO - Clearing user context +2025-12-04 13:21:33,293 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:33,294 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:33,295 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:33,297 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:33,297 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:33,297 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:33,305 - root - INFO - Clearing user context +2025-12-04 13:21:33,307 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 13:21:33,307 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:33,308 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 13:21:33,308 - root - INFO - Clearing user context +2025-12-04 13:21:33,311 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:33,312 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:33,312 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:33,312 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:33,315 - root - INFO - Clearing user context +2025-12-04 13:21:34,847 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 13:21:34,847 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:34,847 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 13:21:34,848 - root - INFO - Clearing user context +2025-12-04 13:21:34,849 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:34,850 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:34,850 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:34,850 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:34,852 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:34,852 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:34,852 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:34,853 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:34,854 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:34,854 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:34,861 - root - INFO - Clearing user context +2025-12-04 13:21:34,871 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 13:21:34,871 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:34,872 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 13:21:34,872 - root - INFO - Clearing user context +2025-12-04 13:21:34,873 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:34,874 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:34,874 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:34,874 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:34,875 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:34,876 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:34,876 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:34,878 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:34,879 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:34,879 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:34,887 - root - INFO - Clearing user context +2025-12-04 13:21:34,898 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 13:21:34,898 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:34,899 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:21:34,899 - root - INFO - Clearing user context +2025-12-04 13:21:34,900 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:34,901 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:34,901 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:34,901 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:34,903 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:34,903 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:34,904 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:34,905 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:34,905 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:34,905 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:34,910 - root - INFO - Clearing user context +2025-12-04 13:21:37,977 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:21:37,977 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:37,977 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:21:37,978 - root - INFO - Clearing user context +2025-12-04 13:21:37,979 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:37,980 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:37,980 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:37,980 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:37,983 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:37,983 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:37,984 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:37,985 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:37,985 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:37,985 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:38,009 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:21:38,010 - root - INFO - Clearing user context +2025-12-04 13:21:43,133 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:21:43,133 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:43,133 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:21:43,134 - root - INFO - Clearing user context +2025-12-04 13:21:43,135 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:43,136 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:43,136 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:43,136 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:43,138 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:43,139 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:43,139 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:43,140 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:43,141 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:43,141 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:43,152 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:21:43,153 - root - INFO - Clearing user context +2025-12-04 13:21:45,966 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 13:21:45,967 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:45,967 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 13:21:45,967 - root - INFO - Clearing user context +2025-12-04 13:21:45,969 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:45,969 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:45,969 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:45,969 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:45,971 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:45,972 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:45,972 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:45,973 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:45,974 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:45,974 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:45,981 - root - INFO - Clearing user context +2025-12-04 13:21:46,974 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 13:21:46,974 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:46,974 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 13:21:46,975 - root - INFO - Clearing user context +2025-12-04 13:21:46,976 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:46,976 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:46,977 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:46,977 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:46,978 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:46,979 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:46,979 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:46,980 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:46,981 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:46,981 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:46,988 - root - INFO - Clearing user context +2025-12-04 13:21:46,999 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 13:21:47,000 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:47,000 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 13:21:47,000 - root - INFO - Clearing user context +2025-12-04 13:21:47,001 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:47,002 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:47,002 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:47,002 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:47,004 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:47,004 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:47,004 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:47,006 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:47,006 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:47,007 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:47,009 - root - INFO - Clearing user context +2025-12-04 13:21:47,026 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 13:21:47,026 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:47,027 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:21:47,027 - root - INFO - Clearing user context +2025-12-04 13:21:47,029 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:47,030 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:47,030 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:47,030 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:47,032 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:47,032 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:47,033 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:47,034 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:47,034 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:47,034 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:47,036 - root - INFO - Clearing user context +2025-12-04 13:21:52,086 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:21:52,086 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:52,086 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:21:52,086 - root - INFO - Clearing user context +2025-12-04 13:21:52,088 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:52,088 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:52,088 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:52,089 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:52,091 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:52,091 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:52,091 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:52,092 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:52,093 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:52,093 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:52,101 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:21:52,102 - root - INFO - Clearing user context +2025-12-04 13:21:54,088 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:21:54,089 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:54,089 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:21:54,089 - root - INFO - Clearing user context +2025-12-04 13:21:54,090 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:54,091 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:54,091 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:54,091 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:54,093 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:54,093 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:54,094 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:54,095 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:54,095 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:54,095 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:54,102 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:21:54,103 - root - INFO - Clearing user context +2025-12-04 13:21:58,301 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:21:58,301 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:21:58,301 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:21:58,302 - root - INFO - Clearing user context +2025-12-04 13:21:58,303 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:21:58,304 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:58,304 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:21:58,304 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:21:58,307 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:21:58,307 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:21:58,307 - root - INFO - Looking for user with username: admin +2025-12-04 13:21:58,309 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:21:58,309 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:21:58,309 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:21:58,319 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:21:58,321 - root - INFO - Clearing user context +2025-12-04 13:26:57,110 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:26:57,110 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:26:57,110 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:26:57,110 - root - INFO - Clearing user context +2025-12-04 13:26:57,112 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:26:57,112 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:26:57,112 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:26:57,113 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:26:57,115 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:26:57,116 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:26:57,116 - root - INFO - Looking for user with username: admin +2025-12-04 13:26:57,118 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:26:57,118 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:26:57,118 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:26:57,126 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:26:57,128 - root - INFO - Clearing user context +2025-12-04 13:27:03,458 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:27:03,458 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:27:03,459 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:27:03,459 - root - INFO - Clearing user context +2025-12-04 13:27:03,460 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:27:03,461 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:03,461 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:27:03,461 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:27:03,463 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:27:03,463 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:27:03,463 - root - INFO - Looking for user with username: admin +2025-12-04 13:27:03,465 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:27:03,465 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:03,465 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:27:03,474 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:27:03,475 - root - INFO - Clearing user context +2025-12-04 13:27:13,258 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:27:13,258 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:27:13,258 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:27:13,259 - root - INFO - Clearing user context +2025-12-04 13:27:13,260 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:27:13,260 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:13,260 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:27:13,261 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:27:13,262 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:27:13,263 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:27:13,263 - root - INFO - Looking for user with username: admin +2025-12-04 13:27:13,264 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:27:13,264 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:13,264 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:27:13,281 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:27:13,282 - root - INFO - Clearing user context +2025-12-04 13:27:15,107 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:27:15,107 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:27:15,108 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:27:15,108 - root - INFO - Clearing user context +2025-12-04 13:27:15,109 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:27:15,110 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:15,110 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:27:15,110 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:27:15,112 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:27:15,112 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:27:15,112 - root - INFO - Looking for user with username: admin +2025-12-04 13:27:15,114 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:27:15,114 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:15,114 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:27:15,121 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:27:15,123 - root - INFO - Clearing user context +2025-12-04 13:27:17,494 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:27:17,495 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:27:17,495 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:27:17,495 - root - INFO - Clearing user context +2025-12-04 13:27:17,497 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:27:17,497 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:17,497 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:27:17,497 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:27:17,499 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:27:17,500 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:27:17,500 - root - INFO - Looking for user with username: admin +2025-12-04 13:27:17,501 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:27:17,501 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:17,501 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:27:17,508 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:27:17,509 - root - INFO - Clearing user context +2025-12-04 13:27:20,474 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 13:27:20,475 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 13:27:20,475 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 13:27:20,475 - root - INFO - Clearing user context +2025-12-04 13:27:20,477 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 13:27:20,477 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:20,477 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 13:27:20,477 - root - INFO - Verified current user ID in context: 2 +2025-12-04 13:27:20,479 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 13:27:20,480 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 13:27:20,480 - root - INFO - Looking for user with username: admin +2025-12-04 13:27:20,481 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 13:27:20,481 - root - INFO - Verification - ContextVar user: admin +2025-12-04 13:27:20,481 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 13:27:20,489 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 13:27:20,490 - root - INFO - Clearing user context +2025-12-04 14:07:57,592 - root - INFO - Logging configured successfully +2025-12-04 14:07:57,705 - root - INFO - Logging configured successfully +2025-12-04 14:07:57,910 - root - INFO - Starting up TH-Agenter application... +2025-12-04 14:07:57,958 - root - INFO - SQLite database engine created: sqlite:///./TH-Agenter.db +2025-12-04 14:07:57,972 - root - INFO - Database tables created +2025-12-04 14:07:57,973 - root - INFO - Database initialized +2025-12-04 14:16:08,129 - root - INFO - [MIDDLEWARE] Processing request: GET /api/auth/me +2025-12-04 14:16:08,129 - root - INFO - [MIDDLEWARE] Checking path: /api/auth/me against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:08,129 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/auth/me +2025-12-04 14:16:08,130 - root - INFO - Clearing user context +2025-12-04 14:16:08,197 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:08,197 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:08,198 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:08,198 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:08,201 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:16:08,202 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:16:08,202 - root - INFO - Looking for user with username: admin +2025-12-04 14:16:08,204 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:16:08,204 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:08,204 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:16:08,222 - root - INFO - Clearing user context +2025-12-04 14:16:08,235 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/roles/user-roles/user/2 +2025-12-04 14:16:08,235 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/roles/user-roles/user/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:08,235 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/roles/user-roles/user/2 +2025-12-04 14:16:08,235 - root - INFO - Clearing user context +2025-12-04 14:16:08,238 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:08,238 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:08,238 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:08,238 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:08,240 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:16:08,241 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:16:08,241 - root - INFO - Looking for user with username: admin +2025-12-04 14:16:08,243 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:16:08,244 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:08,244 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:16:08,250 - root - INFO - Clearing user context +2025-12-04 14:16:09,125 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 14:16:09,126 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:09,126 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 14:16:09,126 - root - INFO - Clearing user context +2025-12-04 14:16:09,127 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:09,127 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,128 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:09,128 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:09,129 - root - INFO - [MIDDLEWARE] Processing request: GET /api/knowledge-bases/ +2025-12-04 14:16:09,129 - root - INFO - [MIDDLEWARE] Checking path: /api/knowledge-bases/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:09,130 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/knowledge-bases/ +2025-12-04 14:16:09,130 - root - INFO - Clearing user context +2025-12-04 14:16:09,132 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:09,132 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,132 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:09,133 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:09,149 - root - INFO - Clearing user context +2025-12-04 14:16:09,150 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:16:09,151 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:16:09,151 - root - INFO - Looking for user with username: admin +2025-12-04 14:16:09,153 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:16:09,154 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,155 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 14:16:09,155 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:16:09,155 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:09,156 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 14:16:09,156 - root - INFO - Clearing user context +2025-12-04 14:16:09,161 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:09,161 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,161 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:09,161 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:09,183 - root - INFO - Clearing user context +2025-12-04 14:16:09,190 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 14:16:09,197 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:09,197 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 14:16:09,197 - root - INFO - Clearing user context +2025-12-04 14:16:09,201 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:09,201 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,202 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:09,202 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:09,203 - root - INFO - Clearing user context +2025-12-04 14:16:09,205 - root - INFO - [MIDDLEWARE] Processing request: GET /api/knowledge-bases/ +2025-12-04 14:16:09,206 - root - INFO - [MIDDLEWARE] Checking path: /api/knowledge-bases/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:09,206 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/knowledge-bases/ +2025-12-04 14:16:09,206 - root - INFO - Clearing user context +2025-12-04 14:16:09,208 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:09,208 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,208 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:09,209 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:09,212 - root - INFO - Clearing user context +2025-12-04 14:16:09,215 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations +2025-12-04 14:16:09,215 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:09,216 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations +2025-12-04 14:16:09,216 - root - INFO - Clearing user context +2025-12-04 14:16:09,218 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:09,218 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,219 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:09,219 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:09,221 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:16:09,222 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:16:09,223 - root - INFO - Looking for user with username: admin +2025-12-04 14:16:09,226 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:16:09,226 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,228 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 14:16:09,228 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:16:09,228 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:09,229 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 14:16:09,229 - root - INFO - Clearing user context +2025-12-04 14:16:09,230 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:09,231 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,231 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:09,231 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:09,232 - root - INFO - Clearing user context +2025-12-04 14:16:09,238 - root - INFO - Clearing user context +2025-12-04 14:16:09,242 - root - INFO - Clearing user context +2025-12-04 14:16:09,251 - root - INFO - [MIDDLEWARE] Processing request: GET /api/chat/conversations/count +2025-12-04 14:16:09,251 - root - INFO - [MIDDLEWARE] Checking path: /api/chat/conversations/count against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:16:09,252 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/chat/conversations/count +2025-12-04 14:16:09,252 - root - INFO - Clearing user context +2025-12-04 14:16:09,253 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:16:09,254 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:16:09,254 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:16:09,254 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:16:09,257 - root - INFO - Clearing user context +2025-12-04 14:22:37,976 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:22:37,977 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:22:37,977 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:22:37,977 - root - INFO - Clearing user context +2025-12-04 14:22:37,979 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:22:37,980 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:37,980 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:22:37,980 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:22:37,982 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:22:37,982 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:22:37,982 - root - INFO - Looking for user with username: admin +2025-12-04 14:22:37,984 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:22:37,984 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:37,984 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:22:38,000 - root - INFO - Clearing user context +2025-12-04 14:22:40,162 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:22:40,163 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:22:40,163 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:22:40,163 - root - INFO - Clearing user context +2025-12-04 14:22:40,165 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:22:40,166 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:40,166 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:22:40,166 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:22:40,168 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:22:40,168 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:22:40,169 - root - INFO - Looking for user with username: admin +2025-12-04 14:22:40,171 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:22:40,171 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:40,171 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:22:40,178 - root - INFO - Clearing user context +2025-12-04 14:22:40,188 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:22:40,188 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:22:40,189 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:22:40,189 - root - INFO - Clearing user context +2025-12-04 14:22:40,190 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:22:40,191 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:40,191 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:22:40,191 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:22:40,193 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:22:40,193 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:22:40,193 - root - INFO - Looking for user with username: admin +2025-12-04 14:22:40,195 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:22:40,195 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:40,195 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:22:40,200 - root - INFO - Clearing user context +2025-12-04 14:22:40,210 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/2 +2025-12-04 14:22:40,210 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:22:40,211 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:22:40,211 - root - INFO - Clearing user context +2025-12-04 14:22:40,212 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:22:40,212 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:40,213 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:22:40,213 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:22:40,214 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:22:40,214 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:22:40,215 - root - INFO - Looking for user with username: admin +2025-12-04 14:22:40,215 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:22:40,216 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:40,216 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:22:40,219 - root - INFO - Clearing user context +2025-12-04 14:22:51,717 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:22:51,717 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:22:51,717 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:22:51,717 - root - INFO - Clearing user context +2025-12-04 14:22:51,719 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:22:51,719 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:51,720 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:22:51,720 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:22:51,722 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:22:51,722 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:22:51,722 - root - INFO - Looking for user with username: admin +2025-12-04 14:22:51,723 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:22:51,724 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:51,724 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:22:51,735 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:22:51,736 - root - INFO - Clearing user context +2025-12-04 14:22:53,744 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:22:53,744 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:22:53,744 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:22:53,744 - root - INFO - Clearing user context +2025-12-04 14:22:53,746 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:22:53,746 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:53,747 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:22:53,747 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:22:53,749 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:22:53,749 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:22:53,749 - root - INFO - Looking for user with username: admin +2025-12-04 14:22:53,751 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:22:53,751 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:53,751 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:22:53,768 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:22:53,770 - root - INFO - Clearing user context +2025-12-04 14:22:57,124 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:22:57,124 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:22:57,125 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:22:57,125 - root - INFO - Clearing user context +2025-12-04 14:22:57,126 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:22:57,126 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:57,127 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:22:57,127 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:22:57,128 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:22:57,129 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:22:57,129 - root - INFO - Looking for user with username: admin +2025-12-04 14:22:57,130 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:22:57,130 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:22:57,131 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:22:57,137 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:22:57,138 - root - INFO - Clearing user context +2025-12-04 14:23:06,944 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:23:06,945 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:23:06,945 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:23:06,945 - root - INFO - Clearing user context +2025-12-04 14:23:06,946 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:23:06,946 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:06,947 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:23:06,947 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:23:06,948 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:23:06,949 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:23:06,949 - root - INFO - Looking for user with username: admin +2025-12-04 14:23:06,950 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:23:06,950 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:06,950 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:23:06,965 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:23:06,967 - root - INFO - Clearing user context +2025-12-04 14:23:12,408 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:23:12,408 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:23:12,408 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:23:12,409 - root - INFO - Clearing user context +2025-12-04 14:23:12,410 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:23:12,410 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:12,410 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:23:12,410 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:23:12,412 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:23:12,412 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:23:12,412 - root - INFO - Looking for user with username: admin +2025-12-04 14:23:12,413 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:23:12,413 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:12,413 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:23:12,421 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:23:12,423 - root - INFO - Clearing user context +2025-12-04 14:23:37,040 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:23:37,040 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:23:37,040 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:23:37,041 - root - INFO - Clearing user context +2025-12-04 14:23:37,042 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:23:37,042 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:37,042 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:23:37,043 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:23:37,044 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:23:37,044 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:23:37,045 - root - INFO - Looking for user with username: admin +2025-12-04 14:23:37,046 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:23:37,046 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:37,046 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:23:37,061 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:23:37,063 - root - INFO - Clearing user context +2025-12-04 14:23:40,765 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:23:40,765 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:23:40,765 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:23:40,766 - root - INFO - Clearing user context +2025-12-04 14:23:40,767 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:23:40,767 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:40,767 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:23:40,767 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:23:40,769 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:23:40,769 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:23:40,770 - root - INFO - Looking for user with username: admin +2025-12-04 14:23:40,771 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:23:40,771 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:40,771 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:23:40,777 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:23:40,778 - root - INFO - Clearing user context +2025-12-04 14:23:53,043 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:23:53,043 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:23:53,044 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:23:53,044 - root - INFO - Clearing user context +2025-12-04 14:23:53,045 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:23:53,045 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:53,045 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:23:53,046 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:23:53,048 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:23:53,049 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:23:53,049 - root - INFO - Looking for user with username: admin +2025-12-04 14:23:53,050 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:23:53,051 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:23:53,051 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:23:53,058 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:23:53,059 - root - INFO - Clearing user context +2025-12-04 14:24:09,610 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:24:09,610 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:24:09,611 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:24:09,611 - root - INFO - Clearing user context +2025-12-04 14:24:09,612 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:24:09,613 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:24:09,613 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:24:09,613 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:24:09,615 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:24:09,615 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:24:09,616 - root - INFO - Looking for user with username: admin +2025-12-04 14:24:09,617 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:24:09,618 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:24:09,618 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:24:09,635 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:24:09,636 - root - INFO - Clearing user context +2025-12-04 14:24:11,846 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:24:11,847 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:24:11,847 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:24:11,847 - root - INFO - Clearing user context +2025-12-04 14:24:11,848 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:24:11,849 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:24:11,849 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:24:11,849 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:24:11,851 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:24:11,851 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:24:11,851 - root - INFO - Looking for user with username: admin +2025-12-04 14:24:11,852 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:24:11,852 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:24:11,852 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:24:11,868 - th_agenter.workflow_api - INFO - Updated workflow: qw3233 by user admin +2025-12-04 14:24:11,869 - root - INFO - Clearing user context +2025-12-04 14:24:15,771 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:24:15,771 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:24:15,771 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:24:15,771 - root - INFO - Clearing user context +2025-12-04 14:24:15,773 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:24:15,773 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:24:15,773 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:24:15,773 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:24:15,775 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:24:15,775 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:24:15,775 - root - INFO - Looking for user with username: admin +2025-12-04 14:24:15,776 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:24:15,776 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:24:15,776 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:24:15,779 - root - ERROR - Database session error: [{'type': 'enum', 'loc': ('body', 'definition', 'nodes', 10, 'type'), 'msg': "Input should be 'start', 'end', 'llm', 'condition', 'loop', 'code', 'http' or 'tool'", 'input': 'weather', 'ctx': {'expected': "'start', 'end', 'llm', 'condition', 'loop', 'code', 'http' or 'tool'"}}] +2025-12-04 14:24:15,780 - root - INFO - Clearing user context +2025-12-04 14:31:43,954 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:31:43,954 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:31:43,955 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:31:43,955 - root - INFO - Clearing user context +2025-12-04 14:31:43,956 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:31:43,956 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:31:43,956 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:31:43,957 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:31:43,958 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:31:43,958 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:31:43,959 - root - INFO - Looking for user with username: admin +2025-12-04 14:31:43,960 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:31:43,960 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:31:43,960 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:31:43,961 - root - ERROR - Database session error: [{'type': 'enum', 'loc': ('body', 'definition', 'nodes', 10, 'type'), 'msg': "Input should be 'start', 'end', 'llm', 'condition', 'loop', 'code', 'http' or 'tool'", 'input': 'weather', 'ctx': {'expected': "'start', 'end', 'llm', 'condition', 'loop', 'code', 'http' or 'tool'"}}] +2025-12-04 14:31:43,962 - root - INFO - Clearing user context +2025-12-04 14:32:20,663 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:32:20,664 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:20,664 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:32:20,664 - root - INFO - Clearing user context +2025-12-04 14:32:20,666 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:20,666 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:20,666 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:20,666 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:20,669 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:20,669 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:20,669 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:20,671 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:20,671 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:20,671 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:20,672 - root - ERROR - Database session error: [{'type': 'enum', 'loc': ('body', 'definition', 'nodes', 10, 'type'), 'msg': "Input should be 'start', 'end', 'llm', 'condition', 'loop', 'code', 'http' or 'tool'", 'input': 'weather', 'ctx': {'expected': "'start', 'end', 'llm', 'condition', 'loop', 'code', 'http' or 'tool'"}}] +2025-12-04 14:32:20,673 - root - INFO - Clearing user context +2025-12-04 14:32:25,181 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/2 +2025-12-04 14:32:25,181 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/2 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:25,181 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/2 +2025-12-04 14:32:25,181 - root - INFO - Clearing user context +2025-12-04 14:32:25,183 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:25,183 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:25,183 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:25,184 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:25,185 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:25,186 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:25,186 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:25,188 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:25,189 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:25,189 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:25,190 - root - ERROR - Database session error: [{'type': 'enum', 'loc': ('body', 'definition', 'nodes', 10, 'type'), 'msg': "Input should be 'start', 'end', 'llm', 'condition', 'loop', 'code', 'http' or 'tool'", 'input': 'weather', 'ctx': {'expected': "'start', 'end', 'llm', 'condition', 'loop', 'code', 'http' or 'tool'"}}] +2025-12-04 14:32:25,191 - root - INFO - Clearing user context +2025-12-04 14:32:36,767 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:32:36,767 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:36,767 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:32:36,767 - root - INFO - Clearing user context +2025-12-04 14:32:36,769 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:36,769 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:36,769 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:36,769 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:36,771 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:36,771 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:36,771 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:36,773 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:36,773 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:36,773 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:36,781 - root - INFO - Clearing user context +2025-12-04 14:32:37,707 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:32:37,708 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:37,708 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:32:37,708 - root - INFO - Clearing user context +2025-12-04 14:32:37,709 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:37,710 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:37,710 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:37,710 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:37,711 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:37,712 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:37,712 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:37,713 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:37,713 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:37,713 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:37,718 - root - INFO - Clearing user context +2025-12-04 14:32:37,726 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:32:37,727 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:37,727 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:32:37,727 - root - INFO - Clearing user context +2025-12-04 14:32:37,728 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:37,728 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:37,729 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:37,729 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:37,730 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:37,730 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:37,730 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:37,731 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:37,731 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:37,732 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:37,734 - root - INFO - Clearing user context +2025-12-04 14:32:37,744 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 14:32:37,745 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:37,745 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:32:37,745 - root - INFO - Clearing user context +2025-12-04 14:32:37,747 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:37,747 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:37,747 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:37,747 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:37,749 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:37,750 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:37,750 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:37,752 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:37,752 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:37,752 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:37,754 - root - INFO - Clearing user context +2025-12-04 14:32:39,843 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:32:39,843 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:39,843 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:32:39,843 - root - INFO - Clearing user context +2025-12-04 14:32:39,845 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:39,845 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:39,845 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:39,846 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:39,847 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:39,847 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:39,848 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:39,849 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:39,849 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:39,849 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:39,865 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:32:39,866 - root - INFO - Clearing user context +2025-12-04 14:32:42,232 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:32:42,233 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:42,233 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:32:42,233 - root - INFO - Clearing user context +2025-12-04 14:32:42,235 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:42,235 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:42,235 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:42,236 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:42,238 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:42,239 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:42,239 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:42,241 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:42,241 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:42,241 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:42,249 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:32:42,250 - root - INFO - Clearing user context +2025-12-04 14:32:54,152 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:32:54,152 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:54,153 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:32:54,153 - root - INFO - Clearing user context +2025-12-04 14:32:54,154 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:54,155 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:54,155 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:54,155 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:54,157 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:54,157 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:54,157 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:54,159 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:54,160 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:54,160 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:54,169 - root - INFO - Clearing user context +2025-12-04 14:32:55,078 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:32:55,079 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:55,079 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:32:55,079 - root - INFO - Clearing user context +2025-12-04 14:32:55,081 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:55,081 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:55,081 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:55,081 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:55,083 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:55,083 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:55,083 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:55,085 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:55,085 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:55,085 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:55,092 - root - INFO - Clearing user context +2025-12-04 14:32:55,099 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:32:55,100 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:55,100 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:32:55,100 - root - INFO - Clearing user context +2025-12-04 14:32:55,102 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:55,102 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:55,102 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:55,102 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:55,104 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:55,104 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:55,104 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:55,106 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:55,106 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:55,106 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:55,109 - root - INFO - Clearing user context +2025-12-04 14:32:55,119 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/8 +2025-12-04 14:32:55,119 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/8 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:55,119 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/8 +2025-12-04 14:32:55,120 - root - INFO - Clearing user context +2025-12-04 14:32:55,122 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:55,122 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:55,122 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:55,122 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:55,124 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:55,124 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:55,125 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:55,127 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:55,127 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:55,127 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:55,129 - root - INFO - Clearing user context +2025-12-04 14:32:56,813 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:32:56,813 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:56,814 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:32:56,814 - root - INFO - Clearing user context +2025-12-04 14:32:56,815 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:56,816 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:56,816 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:56,816 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:56,818 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:56,819 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:56,819 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:56,820 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:56,821 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:56,821 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:56,828 - root - INFO - Clearing user context +2025-12-04 14:32:57,970 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:32:57,970 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:57,970 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:32:57,970 - root - INFO - Clearing user context +2025-12-04 14:32:57,972 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:57,972 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:57,973 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:57,973 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:57,975 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:57,975 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:57,975 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:57,976 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:57,977 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:57,977 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:57,989 - root - INFO - Clearing user context +2025-12-04 14:32:58,003 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:32:58,004 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:58,004 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:32:58,004 - root - INFO - Clearing user context +2025-12-04 14:32:58,006 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:58,007 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:58,007 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:58,007 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:58,008 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:58,009 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:58,009 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:58,010 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:58,011 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:58,011 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:58,013 - root - INFO - Clearing user context +2025-12-04 14:32:58,021 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 14:32:58,021 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:32:58,021 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:32:58,022 - root - INFO - Clearing user context +2025-12-04 14:32:58,024 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:32:58,024 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:58,024 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:32:58,025 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:32:58,026 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:32:58,026 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:32:58,026 - root - INFO - Looking for user with username: admin +2025-12-04 14:32:58,028 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:32:58,029 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:32:58,029 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:32:58,031 - root - INFO - Clearing user context +2025-12-04 14:33:00,493 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:33:00,493 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:00,493 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:33:00,494 - root - INFO - Clearing user context +2025-12-04 14:33:00,495 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:00,495 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:00,495 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:00,495 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:00,497 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:00,497 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:00,497 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:00,498 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:00,499 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:00,499 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:00,505 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:33:00,506 - root - INFO - Clearing user context +2025-12-04 14:33:02,302 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:33:02,302 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:02,302 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:33:02,302 - root - INFO - Clearing user context +2025-12-04 14:33:02,304 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:02,304 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:02,308 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:02,309 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:02,310 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:02,311 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:02,311 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:02,312 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:02,313 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:02,313 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:02,319 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:33:02,320 - root - INFO - Clearing user context +2025-12-04 14:33:02,697 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:33:02,698 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:02,698 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:33:02,698 - root - INFO - Clearing user context +2025-12-04 14:33:02,699 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:02,700 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:02,700 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:02,700 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:02,701 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:02,702 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:02,703 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:02,705 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:02,705 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:02,705 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:02,711 - root - INFO - Clearing user context +2025-12-04 14:33:03,606 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:33:03,606 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:03,606 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:33:03,607 - root - INFO - Clearing user context +2025-12-04 14:33:03,608 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:03,608 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:03,609 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:03,609 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:03,611 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:03,611 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:03,612 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:03,613 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:03,613 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:03,614 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:03,623 - root - INFO - Clearing user context +2025-12-04 14:33:03,630 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:33:03,630 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:03,631 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:33:03,631 - root - INFO - Clearing user context +2025-12-04 14:33:03,633 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:03,633 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:03,633 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:03,634 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:03,635 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:03,636 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:03,636 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:03,638 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:03,638 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:03,639 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:03,642 - root - INFO - Clearing user context +2025-12-04 14:33:03,651 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 14:33:03,652 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:03,652 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:33:03,652 - root - INFO - Clearing user context +2025-12-04 14:33:03,654 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:03,654 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:03,654 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:03,655 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:03,656 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:03,657 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:03,657 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:03,658 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:03,658 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:03,658 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:03,661 - root - INFO - Clearing user context +2025-12-04 14:33:05,789 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:33:05,790 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:05,790 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:33:05,790 - root - INFO - Clearing user context +2025-12-04 14:33:05,792 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:05,792 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:05,792 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:05,792 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:05,794 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:05,794 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:05,794 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:05,795 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:05,795 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:05,795 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:05,801 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:33:05,802 - root - INFO - Clearing user context +2025-12-04 14:33:05,858 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:33:05,858 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:05,859 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:33:05,859 - root - INFO - Clearing user context +2025-12-04 14:33:05,860 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:05,860 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:05,861 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:05,861 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:05,862 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:05,862 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:05,863 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:05,864 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:05,864 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:05,864 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:05,870 - root - INFO - Clearing user context +2025-12-04 14:33:06,668 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:33:06,669 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:06,669 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:33:06,669 - root - INFO - Clearing user context +2025-12-04 14:33:06,670 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:06,671 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:06,671 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:06,671 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:06,673 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:06,673 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:06,674 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:06,675 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:06,675 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:06,675 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:06,682 - root - INFO - Clearing user context +2025-12-04 14:33:06,691 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:33:06,692 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:06,692 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:33:06,692 - root - INFO - Clearing user context +2025-12-04 14:33:06,693 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:06,693 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:06,694 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:06,694 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:06,695 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:06,695 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:06,695 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:06,696 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:06,696 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:06,696 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:06,699 - root - INFO - Clearing user context +2025-12-04 14:33:06,708 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:33:06,709 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:06,709 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:33:06,709 - root - INFO - Clearing user context +2025-12-04 14:33:06,711 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:06,711 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:06,712 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:06,712 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:06,713 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:06,713 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:06,714 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:06,715 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:06,715 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:06,715 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:06,718 - root - INFO - Clearing user context +2025-12-04 14:33:08,426 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:33:08,426 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:08,426 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:33:08,426 - root - INFO - Clearing user context +2025-12-04 14:33:08,428 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:08,428 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:08,428 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:08,428 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:08,430 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:08,430 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:08,430 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:08,431 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:08,432 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:08,432 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:08,438 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:33:08,439 - root - INFO - Clearing user context +2025-12-04 14:33:11,646 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:33:11,646 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:11,646 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:33:11,647 - root - INFO - Clearing user context +2025-12-04 14:33:11,648 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:11,648 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:11,648 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:11,649 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:11,650 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:11,651 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:11,651 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:11,652 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:11,652 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:11,653 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:11,659 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:33:11,660 - root - INFO - Clearing user context +2025-12-04 14:33:33,112 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:33:33,112 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:33,112 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:33:33,113 - root - INFO - Clearing user context +2025-12-04 14:33:33,114 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:33,114 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:33,114 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:33,115 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:33,116 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:33,117 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:33,117 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:33,119 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:33,119 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:33,119 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:33,136 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:33:33,138 - root - INFO - Clearing user context +2025-12-04 14:33:35,869 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:33:35,869 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:35,870 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:33:35,870 - root - INFO - Clearing user context +2025-12-04 14:33:35,871 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:35,871 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:35,872 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:35,872 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:35,873 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:35,874 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:35,874 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:35,875 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:35,875 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:35,876 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:35,882 - root - INFO - Clearing user context +2025-12-04 14:33:37,254 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:33:37,255 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:37,255 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:33:37,255 - root - INFO - Clearing user context +2025-12-04 14:33:37,257 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:37,257 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:37,258 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:37,258 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:37,259 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:37,260 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:37,260 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:37,262 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:37,262 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:37,262 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:37,268 - root - INFO - Clearing user context +2025-12-04 14:33:37,275 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:33:37,276 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:37,276 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:33:37,276 - root - INFO - Clearing user context +2025-12-04 14:33:37,278 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:37,278 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:37,278 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:37,278 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:37,279 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:37,280 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:37,280 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:37,281 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:37,281 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:37,281 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:37,283 - root - INFO - Clearing user context +2025-12-04 14:33:37,291 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/5 +2025-12-04 14:33:37,291 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:37,292 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:33:37,292 - root - INFO - Clearing user context +2025-12-04 14:33:37,294 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:37,294 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:37,294 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:37,294 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:37,296 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:37,296 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:37,296 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:37,298 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:37,299 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:37,299 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:37,301 - root - INFO - Clearing user context +2025-12-04 14:33:38,312 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:33:38,313 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:38,313 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:33:38,313 - root - INFO - Clearing user context +2025-12-04 14:33:38,315 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:38,315 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:38,315 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:38,315 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:38,317 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:38,317 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:38,318 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:38,319 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:38,319 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:38,319 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:38,330 - root - INFO - Clearing user context +2025-12-04 14:33:39,210 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:33:39,211 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:39,211 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:33:39,211 - root - INFO - Clearing user context +2025-12-04 14:33:39,213 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:39,213 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:39,213 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:39,213 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:39,215 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:39,215 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:39,216 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:39,217 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:39,217 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:39,217 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:39,223 - root - INFO - Clearing user context +2025-12-04 14:33:39,230 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:33:39,230 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:39,230 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:33:39,231 - root - INFO - Clearing user context +2025-12-04 14:33:39,232 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:39,232 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:39,232 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:39,232 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:39,233 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:39,234 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:39,234 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:39,235 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:39,235 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:39,235 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:39,239 - root - INFO - Clearing user context +2025-12-04 14:33:39,247 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:33:39,247 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:39,247 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:33:39,248 - root - INFO - Clearing user context +2025-12-04 14:33:39,250 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:39,250 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:39,250 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:39,251 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:39,252 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:39,252 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:39,252 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:39,254 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:39,254 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:39,254 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:39,257 - root - INFO - Clearing user context +2025-12-04 14:33:41,227 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:33:41,227 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:33:41,227 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:33:41,227 - root - INFO - Clearing user context +2025-12-04 14:33:41,228 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:33:41,229 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:41,229 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:33:41,229 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:33:41,230 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:33:41,231 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:33:41,231 - root - INFO - Looking for user with username: admin +2025-12-04 14:33:41,232 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:33:41,232 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:33:41,233 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:33:41,238 - root - INFO - Clearing user context +2025-12-04 14:34:36,973 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:34:36,973 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:34:36,973 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:34:36,973 - root - INFO - Clearing user context +2025-12-04 14:34:36,976 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:34:36,976 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:34:36,976 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:34:36,976 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:34:36,978 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:34:36,978 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:34:36,979 - root - INFO - Looking for user with username: admin +2025-12-04 14:34:36,980 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:34:36,980 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:34:36,981 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:34:36,988 - root - INFO - Clearing user context +2025-12-04 14:34:36,999 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:34:36,999 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:34:36,999 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:34:37,000 - root - INFO - Clearing user context +2025-12-04 14:34:37,002 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:34:37,002 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:34:37,003 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:34:37,003 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:34:37,004 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:34:37,005 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:34:37,005 - root - INFO - Looking for user with username: admin +2025-12-04 14:34:37,006 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:34:37,007 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:34:37,007 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:34:37,011 - root - INFO - Clearing user context +2025-12-04 14:34:37,019 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:34:37,019 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:34:37,019 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:34:37,020 - root - INFO - Clearing user context +2025-12-04 14:34:37,021 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:34:37,021 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:34:37,021 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:34:37,022 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:34:37,023 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:34:37,024 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:34:37,024 - root - INFO - Looking for user with username: admin +2025-12-04 14:34:37,026 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:34:37,026 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:34:37,026 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:34:37,029 - root - INFO - Clearing user context +2025-12-04 14:34:44,248 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:34:44,248 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:34:44,249 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:34:44,249 - root - INFO - Clearing user context +2025-12-04 14:34:44,250 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:34:44,250 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:34:44,251 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:34:44,251 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:34:44,253 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:34:44,253 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:34:44,253 - root - INFO - Looking for user with username: admin +2025-12-04 14:34:44,254 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:34:44,255 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:34:44,255 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:34:44,272 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:34:44,273 - root - INFO - Clearing user context +2025-12-04 14:35:00,349 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:00,349 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:00,350 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:00,350 - root - INFO - Clearing user context +2025-12-04 14:35:00,351 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:00,352 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:00,352 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:00,352 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:00,354 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:00,355 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:00,355 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:00,356 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:00,356 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:00,357 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:00,362 - root - INFO - Clearing user context +2025-12-04 14:35:00,385 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:35:00,386 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:00,386 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:35:00,386 - root - INFO - Clearing user context +2025-12-04 14:35:00,387 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:00,387 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:00,388 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:00,388 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:00,389 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:00,389 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:00,390 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:00,391 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:00,391 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:00,391 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:00,394 - root - INFO - Clearing user context +2025-12-04 14:35:00,400 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:35:00,400 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:00,401 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:35:00,401 - root - INFO - Clearing user context +2025-12-04 14:35:00,402 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:00,402 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:00,402 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:00,402 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:00,403 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:00,404 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:00,404 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:00,405 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:00,405 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:00,405 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:00,407 - root - INFO - Clearing user context +2025-12-04 14:35:13,567 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:13,568 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:13,568 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:13,568 - root - INFO - Clearing user context +2025-12-04 14:35:13,570 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:13,570 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:13,570 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:13,571 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:13,572 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:13,572 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:13,572 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:13,574 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:13,574 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:13,574 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:13,582 - root - INFO - Clearing user context +2025-12-04 14:35:14,492 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:14,492 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:14,493 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:14,493 - root - INFO - Clearing user context +2025-12-04 14:35:14,494 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:14,495 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:14,495 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:14,495 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:14,496 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:14,497 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:14,497 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:14,498 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:14,499 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:14,499 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:14,506 - root - INFO - Clearing user context +2025-12-04 14:35:14,513 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:35:14,514 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:14,514 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:35:14,514 - root - INFO - Clearing user context +2025-12-04 14:35:14,515 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:14,516 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:14,516 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:14,516 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:14,517 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:14,518 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:14,518 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:14,519 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:14,520 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:14,520 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:14,523 - root - INFO - Clearing user context +2025-12-04 14:35:14,533 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/8 +2025-12-04 14:35:14,533 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/8 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:14,533 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/8 +2025-12-04 14:35:14,533 - root - INFO - Clearing user context +2025-12-04 14:35:14,535 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:14,536 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:14,536 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:14,536 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:14,538 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:14,538 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:14,538 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:14,540 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:14,540 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:14,540 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:14,542 - root - INFO - Clearing user context +2025-12-04 14:35:15,825 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:15,825 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:15,826 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:15,826 - root - INFO - Clearing user context +2025-12-04 14:35:15,827 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:15,827 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:15,828 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:15,828 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:15,829 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:15,830 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:15,830 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:15,831 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:15,832 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:15,832 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:15,838 - root - INFO - Clearing user context +2025-12-04 14:35:16,606 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:16,607 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:16,607 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:16,607 - root - INFO - Clearing user context +2025-12-04 14:35:16,609 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:16,609 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:16,609 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:16,609 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:16,611 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:16,611 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:16,612 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:16,613 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:16,614 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:16,614 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:16,624 - root - INFO - Clearing user context +2025-12-04 14:35:16,632 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:35:16,633 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:16,633 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:35:16,633 - root - INFO - Clearing user context +2025-12-04 14:35:16,634 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:16,635 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:16,635 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:16,635 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:16,637 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:16,638 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:16,638 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:16,640 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:16,640 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:16,640 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:16,645 - root - INFO - Clearing user context +2025-12-04 14:35:16,652 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 14:35:16,652 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:16,653 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:35:16,653 - root - INFO - Clearing user context +2025-12-04 14:35:16,655 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:16,655 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:16,655 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:16,656 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:16,657 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:16,658 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:16,658 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:16,660 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:16,661 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:16,661 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:16,664 - root - INFO - Clearing user context +2025-12-04 14:35:17,431 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:17,431 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:17,432 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:17,432 - root - INFO - Clearing user context +2025-12-04 14:35:17,433 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:17,433 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:17,434 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:17,434 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:17,435 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:17,435 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:17,435 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:17,437 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:17,437 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:17,438 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:17,445 - root - INFO - Clearing user context +2025-12-04 14:35:17,473 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:35:17,473 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:17,474 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:35:17,474 - root - INFO - Clearing user context +2025-12-04 14:35:17,476 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:17,476 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:17,476 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:17,476 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:17,477 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:17,478 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:17,478 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:17,479 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:17,479 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:17,480 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:17,482 - root - INFO - Clearing user context +2025-12-04 14:35:17,492 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 14:35:17,492 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:17,492 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:35:17,493 - root - INFO - Clearing user context +2025-12-04 14:35:17,494 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:17,494 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:17,494 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:17,494 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:17,496 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:17,496 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:17,497 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:17,498 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:17,498 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:17,498 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:17,500 - root - INFO - Clearing user context +2025-12-04 14:35:18,139 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:18,139 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:18,140 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:18,140 - root - INFO - Clearing user context +2025-12-04 14:35:18,141 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:18,142 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:18,142 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:18,142 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:18,144 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:18,144 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:18,144 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:18,145 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:18,146 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:18,146 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:18,152 - root - INFO - Clearing user context +2025-12-04 14:35:18,893 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:18,894 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:18,895 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:18,896 - root - INFO - Clearing user context +2025-12-04 14:35:18,898 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:18,899 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:18,899 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:18,899 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:18,901 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:18,902 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:18,902 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:18,903 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:18,904 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:18,904 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:18,914 - root - INFO - Clearing user context +2025-12-04 14:35:18,925 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:35:18,925 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:18,925 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:35:18,926 - root - INFO - Clearing user context +2025-12-04 14:35:18,927 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:18,928 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:18,928 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:18,928 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:18,930 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:18,931 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:18,931 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:18,932 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:18,933 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:18,933 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:18,937 - root - INFO - Clearing user context +2025-12-04 14:35:18,947 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:35:18,947 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:18,948 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:35:18,948 - root - INFO - Clearing user context +2025-12-04 14:35:18,949 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:18,950 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:18,950 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:18,950 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:18,952 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:18,952 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:18,952 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:18,954 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:18,955 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:18,955 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:18,958 - root - INFO - Clearing user context +2025-12-04 14:35:24,108 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:35:24,109 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:24,109 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:35:24,109 - root - INFO - Clearing user context +2025-12-04 14:35:24,111 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:24,111 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:24,112 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:24,112 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:24,115 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:24,115 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:24,115 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:24,117 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:24,117 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:24,117 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:24,126 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:35:24,128 - root - INFO - Clearing user context +2025-12-04 14:35:28,778 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:28,779 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:28,779 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:28,779 - root - INFO - Clearing user context +2025-12-04 14:35:28,781 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:28,781 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:28,781 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:28,781 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:28,783 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:28,783 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:28,783 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:28,785 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:28,785 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:28,785 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:28,792 - root - INFO - Clearing user context +2025-12-04 14:35:29,636 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:29,636 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:29,637 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:29,637 - root - INFO - Clearing user context +2025-12-04 14:35:29,638 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:29,639 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:29,639 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:29,639 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:29,641 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:29,641 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:29,641 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:29,643 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:29,643 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:29,643 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:29,649 - root - INFO - Clearing user context +2025-12-04 14:35:29,656 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:35:29,657 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:29,657 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:35:29,657 - root - INFO - Clearing user context +2025-12-04 14:35:29,659 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:29,659 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:29,660 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:29,660 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:29,662 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:29,663 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:29,663 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:29,666 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:29,669 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:29,669 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:29,673 - root - INFO - Clearing user context +2025-12-04 14:35:29,684 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 14:35:29,684 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:29,685 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:35:29,685 - root - INFO - Clearing user context +2025-12-04 14:35:29,687 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:29,688 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:29,688 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:29,688 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:29,690 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:29,691 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:29,691 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:29,693 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:29,693 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:29,694 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:29,697 - root - INFO - Clearing user context +2025-12-04 14:35:31,809 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:31,809 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:31,810 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:31,810 - root - INFO - Clearing user context +2025-12-04 14:35:31,811 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:31,812 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:31,812 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:31,812 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:31,814 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:31,814 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:31,814 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:31,815 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:31,815 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:31,816 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:31,823 - root - INFO - Clearing user context +2025-12-04 14:35:32,636 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:35:32,636 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:32,636 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:35:32,636 - root - INFO - Clearing user context +2025-12-04 14:35:32,638 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:32,638 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:32,638 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:32,639 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:32,640 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:32,640 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:32,641 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:32,642 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:32,642 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:32,642 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:32,650 - root - INFO - Clearing user context +2025-12-04 14:35:32,657 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:35:32,658 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:32,658 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:35:32,658 - root - INFO - Clearing user context +2025-12-04 14:35:32,660 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:32,660 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:32,661 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:32,661 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:32,662 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:32,663 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:32,663 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:32,664 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:32,665 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:32,665 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:32,668 - root - INFO - Clearing user context +2025-12-04 14:35:32,678 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:35:32,679 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:32,679 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:35:32,679 - root - INFO - Clearing user context +2025-12-04 14:35:32,681 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:32,681 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:32,682 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:32,682 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:32,683 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:32,684 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:32,684 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:32,686 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:32,686 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:32,687 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:32,689 - root - INFO - Clearing user context +2025-12-04 14:35:39,433 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:35:39,433 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:39,434 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:35:39,434 - root - INFO - Clearing user context +2025-12-04 14:35:39,435 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:39,435 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:39,435 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:39,435 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:39,437 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:39,437 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:39,437 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:39,439 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:39,439 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:39,439 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:39,456 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:35:39,457 - root - INFO - Clearing user context +2025-12-04 14:35:50,180 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:35:50,180 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:50,181 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:35:50,181 - root - INFO - Clearing user context +2025-12-04 14:35:50,182 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:50,182 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:50,183 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:50,183 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:50,184 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:50,185 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:50,185 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:50,186 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:50,186 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:50,186 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:50,192 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:35:50,193 - root - INFO - Clearing user context +2025-12-04 14:35:53,210 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:35:53,210 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:53,210 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:35:53,211 - root - INFO - Clearing user context +2025-12-04 14:35:53,212 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:53,212 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:53,212 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:53,213 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:53,214 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:53,214 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:53,214 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:53,216 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:53,216 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:53,216 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:53,222 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:35:53,223 - root - INFO - Clearing user context +2025-12-04 14:35:56,611 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:35:56,611 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:56,612 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:35:56,612 - root - INFO - Clearing user context +2025-12-04 14:35:56,613 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:56,614 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:56,614 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:56,614 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:56,616 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:56,616 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:56,617 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:56,618 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:56,618 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:56,619 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:56,635 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:35:56,636 - root - INFO - Clearing user context +2025-12-04 14:35:58,898 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:35:58,898 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:35:58,899 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:35:58,899 - root - INFO - Clearing user context +2025-12-04 14:35:58,900 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:35:58,900 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:58,900 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:35:58,901 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:35:58,902 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:35:58,902 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:35:58,902 - root - INFO - Looking for user with username: admin +2025-12-04 14:35:58,903 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:35:58,904 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:35:58,904 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:35:58,911 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:35:58,913 - root - INFO - Clearing user context +2025-12-04 14:36:03,187 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:36:03,188 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:03,188 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:36:03,188 - root - INFO - Clearing user context +2025-12-04 14:36:03,190 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:03,190 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:03,190 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:03,191 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:03,193 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:03,193 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:03,194 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:03,195 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:03,196 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:03,196 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:03,203 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:36:03,205 - root - INFO - Clearing user context +2025-12-04 14:36:06,891 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:36:06,891 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:06,892 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:36:06,892 - root - INFO - Clearing user context +2025-12-04 14:36:06,893 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:06,893 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:06,894 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:06,894 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:06,895 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:06,896 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:06,896 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:06,897 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:06,898 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:06,898 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:06,914 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:36:06,915 - root - INFO - Clearing user context +2025-12-04 14:36:13,633 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:36:13,633 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:13,633 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:36:13,634 - root - INFO - Clearing user context +2025-12-04 14:36:13,635 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:13,635 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:13,635 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:13,636 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:13,637 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:13,638 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:13,638 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:13,640 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:13,640 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:13,640 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:13,658 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:36:13,660 - root - INFO - Clearing user context +2025-12-04 14:36:21,352 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:36:21,352 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:21,352 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:36:21,353 - root - INFO - Clearing user context +2025-12-04 14:36:21,354 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:21,354 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:21,354 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:21,354 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:21,356 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:21,356 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:21,356 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:21,357 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:21,358 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:21,358 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:21,373 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:36:21,375 - root - INFO - Clearing user context +2025-12-04 14:36:26,419 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:36:26,419 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:26,420 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:36:26,420 - root - INFO - Clearing user context +2025-12-04 14:36:26,421 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:26,426 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:26,426 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:26,426 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:26,428 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:26,429 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:26,429 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:26,431 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:26,431 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:26,432 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:26,439 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:36:26,440 - root - INFO - Clearing user context +2025-12-04 14:36:36,411 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:36:36,412 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:36,412 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:36:36,412 - root - INFO - Clearing user context +2025-12-04 14:36:36,413 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:36,414 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:36,414 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:36,414 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:36,415 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:36,416 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:36,416 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:36,417 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:36,417 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:36,417 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:36,423 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:36:36,425 - root - INFO - Clearing user context +2025-12-04 14:36:38,414 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:36:38,414 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:38,414 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:36:38,415 - root - INFO - Clearing user context +2025-12-04 14:36:38,416 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:38,416 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:38,417 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:38,417 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:38,420 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:38,420 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:38,420 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:38,422 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:38,422 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:38,422 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:38,428 - root - INFO - Clearing user context +2025-12-04 14:36:39,360 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:36:39,361 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:39,361 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:36:39,361 - root - INFO - Clearing user context +2025-12-04 14:36:39,363 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:39,363 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:39,363 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:39,363 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:39,365 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:39,365 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:39,365 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:39,366 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:39,367 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:39,367 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:39,376 - root - INFO - Clearing user context +2025-12-04 14:36:39,383 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:36:39,383 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:39,384 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:36:39,384 - root - INFO - Clearing user context +2025-12-04 14:36:39,385 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:39,385 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:39,386 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:39,386 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:39,387 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:39,387 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:39,388 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:39,389 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:39,389 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:39,389 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:39,392 - root - INFO - Clearing user context +2025-12-04 14:36:39,399 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:36:39,399 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:39,399 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:36:39,399 - root - INFO - Clearing user context +2025-12-04 14:36:39,401 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:39,401 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:39,401 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:39,401 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:39,402 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:39,403 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:39,403 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:39,405 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:39,405 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:39,405 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:39,408 - root - INFO - Clearing user context +2025-12-04 14:36:41,348 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:36:41,348 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:41,349 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:36:41,349 - root - INFO - Clearing user context +2025-12-04 14:36:41,350 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:41,350 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:41,350 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:41,351 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:41,352 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:41,352 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:41,352 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:41,353 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:41,354 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:41,354 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:41,369 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:36:41,371 - root - INFO - Clearing user context +2025-12-04 14:36:41,840 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:36:41,841 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:41,841 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:36:41,841 - root - INFO - Clearing user context +2025-12-04 14:36:41,843 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:41,843 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:41,843 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:41,843 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:41,845 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:41,845 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:41,845 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:41,847 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:41,847 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:41,847 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:41,853 - root - INFO - Clearing user context +2025-12-04 14:36:42,758 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:36:42,759 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:42,759 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:36:42,759 - root - INFO - Clearing user context +2025-12-04 14:36:42,760 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:42,760 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:42,761 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:42,761 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:42,763 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:42,763 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:42,763 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:42,764 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:42,765 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:42,765 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:42,771 - root - INFO - Clearing user context +2025-12-04 14:36:42,780 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:36:42,781 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:42,781 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:36:42,781 - root - INFO - Clearing user context +2025-12-04 14:36:42,782 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:42,783 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:42,783 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:42,783 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:42,784 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:42,785 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:42,785 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:42,786 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:42,786 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:42,786 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:42,788 - root - INFO - Clearing user context +2025-12-04 14:36:42,797 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/5 +2025-12-04 14:36:42,797 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:42,797 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:36:42,798 - root - INFO - Clearing user context +2025-12-04 14:36:42,799 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:42,800 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:42,800 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:42,800 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:42,801 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:42,802 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:42,802 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:42,803 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:42,803 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:42,803 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:42,806 - root - INFO - Clearing user context +2025-12-04 14:36:45,510 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:36:45,510 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:45,511 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:36:45,511 - root - INFO - Clearing user context +2025-12-04 14:36:45,513 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:45,513 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:45,514 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:45,514 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:45,515 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:45,516 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:45,516 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:45,517 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:45,517 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:45,518 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:45,525 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:36:45,526 - root - INFO - Clearing user context +2025-12-04 14:36:48,047 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:36:48,047 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:48,048 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:36:48,048 - root - INFO - Clearing user context +2025-12-04 14:36:48,049 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:48,049 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:48,050 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:48,050 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:48,051 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:48,052 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:48,052 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:48,053 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:48,053 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:48,053 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:48,070 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:36:48,071 - root - INFO - Clearing user context +2025-12-04 14:36:52,403 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:36:52,403 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:52,403 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:36:52,404 - root - INFO - Clearing user context +2025-12-04 14:36:52,405 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:52,405 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:52,405 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:52,405 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:52,407 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:52,407 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:52,407 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:52,408 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:52,408 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:52,409 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:52,421 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:36:52,422 - root - INFO - Clearing user context +2025-12-04 14:36:55,635 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:36:55,635 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:55,635 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:36:55,636 - root - INFO - Clearing user context +2025-12-04 14:36:55,637 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:55,637 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:55,638 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:55,638 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:55,639 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:55,640 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:55,640 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:55,641 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:55,641 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:55,641 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:55,650 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:36:55,651 - root - INFO - Clearing user context +2025-12-04 14:36:58,967 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:36:58,968 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:36:58,968 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:36:58,968 - root - INFO - Clearing user context +2025-12-04 14:36:58,969 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:36:58,970 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:58,970 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:36:58,970 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:36:58,972 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:36:58,972 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:36:58,972 - root - INFO - Looking for user with username: admin +2025-12-04 14:36:58,973 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:36:58,973 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:36:58,974 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:36:58,981 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:36:58,982 - root - INFO - Clearing user context +2025-12-04 14:37:11,007 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:37:11,007 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:11,007 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:11,008 - root - INFO - Clearing user context +2025-12-04 14:37:11,009 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:11,009 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:11,009 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:11,009 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:11,011 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:11,011 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:11,011 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:11,013 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:11,013 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:11,013 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:11,020 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:37:11,021 - root - INFO - Clearing user context +2025-12-04 14:37:17,724 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:37:17,724 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:17,724 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:17,725 - root - INFO - Clearing user context +2025-12-04 14:37:17,726 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:17,726 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:17,726 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:17,727 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:17,728 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:17,729 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:17,729 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:17,731 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:17,732 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:17,732 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:17,748 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:37:17,749 - root - INFO - Clearing user context +2025-12-04 14:37:28,102 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:37:28,102 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:28,102 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:28,103 - root - INFO - Clearing user context +2025-12-04 14:37:28,104 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:28,104 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:28,104 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:28,104 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:28,106 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:28,106 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:28,106 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:28,107 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:28,107 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:28,107 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:28,114 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:37:28,114 - root - INFO - Clearing user context +2025-12-04 14:37:29,741 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:37:29,741 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:29,742 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:29,742 - root - INFO - Clearing user context +2025-12-04 14:37:29,743 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:29,743 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:29,743 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:29,744 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:29,745 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:29,746 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:29,746 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:29,747 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:29,747 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:29,747 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:29,754 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:37:29,755 - root - INFO - Clearing user context +2025-12-04 14:37:34,915 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:37:34,916 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:34,916 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:34,916 - root - INFO - Clearing user context +2025-12-04 14:37:34,917 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:34,918 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:34,918 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:34,918 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:34,919 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:34,920 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:34,920 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:34,921 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:34,921 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:34,921 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:34,936 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:37:34,937 - root - INFO - Clearing user context +2025-12-04 14:37:37,250 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:37:37,250 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:37,250 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:37,250 - root - INFO - Clearing user context +2025-12-04 14:37:37,252 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:37,252 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:37,252 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:37,253 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:37,254 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:37,254 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:37,255 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:37,256 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:37,256 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:37,256 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:37,263 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:37:37,264 - root - INFO - Clearing user context +2025-12-04 14:37:42,173 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:37:42,173 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:42,173 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:42,173 - root - INFO - Clearing user context +2025-12-04 14:37:42,175 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:42,175 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:42,175 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:42,175 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:42,176 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:42,177 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:42,177 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:42,178 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:42,178 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:42,178 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:42,195 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:37:42,197 - root - INFO - Clearing user context +2025-12-04 14:37:45,619 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:37:45,619 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:45,620 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:45,620 - root - INFO - Clearing user context +2025-12-04 14:37:45,621 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:45,621 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:45,622 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:45,622 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:45,623 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:45,624 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:45,624 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:45,625 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:45,625 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:45,625 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:45,641 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:37:45,642 - root - INFO - Clearing user context +2025-12-04 14:37:50,501 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/5 +2025-12-04 14:37:50,501 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:50,501 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:50,502 - root - INFO - Clearing user context +2025-12-04 14:37:50,503 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:50,503 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:50,503 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:50,503 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:50,505 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:50,505 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:50,505 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:50,507 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:50,507 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:50,507 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:50,515 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 - 副本 by user admin +2025-12-04 14:37:50,516 - root - INFO - Clearing user context +2025-12-04 14:37:57,432 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:37:57,433 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:57,433 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:37:57,433 - root - INFO - Clearing user context +2025-12-04 14:37:57,434 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:57,434 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:57,434 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:57,435 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:57,436 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:57,436 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:57,436 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:57,437 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:57,437 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:57,437 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:57,442 - root - INFO - Clearing user context +2025-12-04 14:37:57,451 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:37:57,452 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:57,452 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:37:57,452 - root - INFO - Clearing user context +2025-12-04 14:37:57,453 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:57,453 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:57,453 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:57,454 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:57,455 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:57,455 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:57,455 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:57,456 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:57,456 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:57,456 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:57,458 - root - INFO - Clearing user context +2025-12-04 14:37:57,467 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/5 +2025-12-04 14:37:57,467 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/5 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:37:57,468 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/5 +2025-12-04 14:37:57,468 - root - INFO - Clearing user context +2025-12-04 14:37:57,469 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:37:57,469 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:57,470 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:37:57,470 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:37:57,472 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:37:57,472 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:37:57,472 - root - INFO - Looking for user with username: admin +2025-12-04 14:37:57,474 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:37:57,474 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:37:57,474 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:37:57,477 - root - INFO - Clearing user context +2025-12-04 14:38:00,991 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:38:00,991 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:00,991 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:38:00,992 - root - INFO - Clearing user context +2025-12-04 14:38:00,993 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:00,994 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:00,994 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:00,994 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:00,996 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:00,996 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:00,997 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:00,998 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:00,999 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:00,999 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:01,009 - root - INFO - Clearing user context +2025-12-04 14:38:03,751 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:38:03,751 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:03,752 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:38:03,752 - root - INFO - Clearing user context +2025-12-04 14:38:03,753 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:03,753 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:03,754 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:03,754 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:03,755 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:03,756 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:03,757 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:03,758 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:03,759 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:03,759 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:03,766 - root - INFO - Clearing user context +2025-12-04 14:38:03,774 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:38:03,774 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:03,774 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:38:03,775 - root - INFO - Clearing user context +2025-12-04 14:38:03,776 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:03,776 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:03,777 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:03,777 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:03,778 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:03,779 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:03,779 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:03,782 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:03,782 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:03,782 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:03,786 - root - INFO - Clearing user context +2025-12-04 14:38:03,793 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:38:03,794 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:03,794 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:38:03,794 - root - INFO - Clearing user context +2025-12-04 14:38:03,796 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:03,796 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:03,796 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:03,796 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:03,798 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:03,799 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:03,799 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:03,801 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:03,801 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:03,802 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:03,804 - root - INFO - Clearing user context +2025-12-04 14:38:05,861 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:38:05,861 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:05,861 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:38:05,862 - root - INFO - Clearing user context +2025-12-04 14:38:05,863 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:05,863 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:05,864 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:05,864 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:05,865 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:05,866 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:05,866 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:05,867 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:05,867 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:05,867 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:05,874 - root - INFO - Clearing user context +2025-12-04 14:38:07,092 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:38:07,093 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:07,093 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:38:07,093 - root - INFO - Clearing user context +2025-12-04 14:38:07,096 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:07,096 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:07,096 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:07,096 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:07,098 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:07,098 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:07,099 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:07,100 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:07,100 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:07,100 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:07,108 - root - INFO - Clearing user context +2025-12-04 14:38:07,118 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:38:07,118 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:07,118 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:38:07,118 - root - INFO - Clearing user context +2025-12-04 14:38:07,120 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:07,120 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:07,120 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:07,120 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:07,122 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:07,122 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:07,122 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:07,123 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:07,124 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:07,124 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:07,126 - root - INFO - Clearing user context +2025-12-04 14:38:07,135 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 14:38:07,136 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:07,136 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:38:07,136 - root - INFO - Clearing user context +2025-12-04 14:38:07,138 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:07,138 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:07,139 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:07,139 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:07,140 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:07,140 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:07,140 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:07,142 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:07,142 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:07,142 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:07,144 - root - INFO - Clearing user context +2025-12-04 14:38:10,314 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:38:10,314 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:10,314 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:38:10,315 - root - INFO - Clearing user context +2025-12-04 14:38:10,316 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:10,317 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:10,317 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:10,317 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:10,319 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:10,319 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:10,320 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:10,321 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:10,321 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:10,321 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:10,329 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:38:10,330 - root - INFO - Clearing user context +2025-12-04 14:38:11,872 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:38:11,872 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:11,872 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:38:11,872 - root - INFO - Clearing user context +2025-12-04 14:38:11,874 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:11,874 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:11,874 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:11,874 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:11,876 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:11,876 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:11,877 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:11,878 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:11,878 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:11,878 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:11,885 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:38:11,886 - root - INFO - Clearing user context +2025-12-04 14:38:18,109 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:38:18,110 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:18,110 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:38:18,110 - root - INFO - Clearing user context +2025-12-04 14:38:18,111 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:18,112 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:18,112 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:18,112 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:18,114 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:18,114 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:18,114 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:18,115 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:18,115 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:18,116 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:18,123 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:38:18,124 - root - INFO - Clearing user context +2025-12-04 14:38:20,386 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:38:20,386 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:20,386 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:38:20,386 - root - INFO - Clearing user context +2025-12-04 14:38:20,388 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:20,388 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:20,388 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:20,388 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:20,390 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:20,390 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:20,390 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:20,391 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:20,391 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:20,392 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:20,408 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:38:20,409 - root - INFO - Clearing user context +2025-12-04 14:38:26,002 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:38:26,003 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:38:26,003 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:38:26,003 - root - INFO - Clearing user context +2025-12-04 14:38:26,005 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:38:26,005 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:26,005 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:38:26,005 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:38:26,007 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:38:26,007 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:38:26,007 - root - INFO - Looking for user with username: admin +2025-12-04 14:38:26,009 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:38:26,009 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:38:26,009 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:38:26,026 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:38:26,027 - root - INFO - Clearing user context +2025-12-04 14:39:19,598 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:39:19,599 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:19,599 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:39:19,599 - root - INFO - Clearing user context +2025-12-04 14:39:19,600 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:19,601 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:19,601 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:19,601 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:19,603 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:19,604 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:19,604 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:19,605 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:19,605 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:19,606 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:19,617 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:39:19,619 - root - INFO - Clearing user context +2025-12-04 14:39:24,135 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:39:24,136 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:24,136 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:39:24,136 - root - INFO - Clearing user context +2025-12-04 14:39:24,138 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:24,138 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:24,138 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:24,139 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:24,140 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:24,141 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:24,141 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:24,143 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:24,143 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:24,143 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:24,152 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:39:24,154 - root - INFO - Clearing user context +2025-12-04 14:39:26,208 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:39:26,208 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:26,209 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:39:26,209 - root - INFO - Clearing user context +2025-12-04 14:39:26,210 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:26,210 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:26,211 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:26,211 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:26,213 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:26,213 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:26,213 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:26,215 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:26,215 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:26,215 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:26,223 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:39:26,224 - root - INFO - Clearing user context +2025-12-04 14:39:28,072 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:39:28,072 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:28,073 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:39:28,073 - root - INFO - Clearing user context +2025-12-04 14:39:28,074 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:28,075 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:28,075 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:28,075 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:28,077 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:28,078 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:28,078 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:28,079 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:28,080 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:28,080 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:28,088 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:39:28,089 - root - INFO - Clearing user context +2025-12-04 14:39:31,310 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:39:31,311 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:31,311 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:39:31,311 - root - INFO - Clearing user context +2025-12-04 14:39:31,313 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:31,313 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:31,313 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:31,313 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:31,315 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:31,316 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:31,316 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:31,317 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:31,318 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:31,318 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:31,327 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:39:31,328 - root - INFO - Clearing user context +2025-12-04 14:39:35,917 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:39:35,917 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:35,918 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:39:35,918 - root - INFO - Clearing user context +2025-12-04 14:39:35,919 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:35,919 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:35,919 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:35,920 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:35,921 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:35,921 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:35,922 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:35,922 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:35,923 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:35,923 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:35,929 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:39:35,930 - root - INFO - Clearing user context +2025-12-04 14:39:37,771 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:39:37,771 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:37,771 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:39:37,771 - root - INFO - Clearing user context +2025-12-04 14:39:37,773 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:37,773 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:37,773 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:37,773 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:37,774 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:37,775 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:37,775 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:37,776 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:37,776 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:37,776 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:37,782 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:39:37,783 - root - INFO - Clearing user context +2025-12-04 14:39:40,220 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:39:40,220 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:40,221 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:39:40,221 - root - INFO - Clearing user context +2025-12-04 14:39:40,222 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:40,222 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:40,223 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:40,223 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:40,224 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:40,225 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:40,225 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:40,226 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:40,226 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:40,226 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:40,233 - root - INFO - Clearing user context +2025-12-04 14:39:41,144 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:39:41,144 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:41,144 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:39:41,145 - root - INFO - Clearing user context +2025-12-04 14:39:41,146 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:41,147 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:41,147 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:41,147 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:41,149 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:41,149 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:41,150 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:41,151 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:41,151 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:41,152 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:41,156 - root - INFO - Clearing user context +2025-12-04 14:39:41,165 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:39:41,165 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:41,166 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:39:41,166 - root - INFO - Clearing user context +2025-12-04 14:39:41,168 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:41,169 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:41,169 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:41,169 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:41,171 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:41,171 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:41,171 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:41,172 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:41,173 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:41,173 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:41,175 - root - INFO - Clearing user context +2025-12-04 14:39:41,184 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:39:41,184 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:41,185 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:39:41,185 - root - INFO - Clearing user context +2025-12-04 14:39:41,187 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:41,187 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:41,187 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:41,187 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:41,189 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:41,189 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:41,189 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:41,191 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:41,191 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:41,192 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:41,194 - root - INFO - Clearing user context +2025-12-04 14:39:45,242 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:39:45,242 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:45,243 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:39:45,243 - root - INFO - Clearing user context +2025-12-04 14:39:45,244 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:45,244 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:45,244 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:45,245 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:45,246 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:45,246 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:45,246 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:45,247 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:45,248 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:45,248 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:45,262 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:39:45,263 - root - INFO - Clearing user context +2025-12-04 14:39:45,854 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:39:45,855 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:45,855 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:39:45,855 - root - INFO - Clearing user context +2025-12-04 14:39:45,856 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:45,857 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:45,857 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:45,857 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:45,859 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:45,859 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:45,859 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:45,860 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:45,860 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:45,861 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:45,867 - root - INFO - Clearing user context +2025-12-04 14:39:46,893 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:39:46,894 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:46,894 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:39:46,894 - root - INFO - Clearing user context +2025-12-04 14:39:46,896 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:46,896 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:46,896 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:46,896 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:46,898 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:46,899 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:46,900 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:46,902 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:46,903 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:46,903 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:46,913 - root - INFO - Clearing user context +2025-12-04 14:39:46,926 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:39:46,926 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:46,926 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:39:46,926 - root - INFO - Clearing user context +2025-12-04 14:39:46,928 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:46,928 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:46,928 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:46,928 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:46,930 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:46,931 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:46,931 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:46,932 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:46,933 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:46,933 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:46,938 - root - INFO - Clearing user context +2025-12-04 14:39:46,948 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 14:39:46,948 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:46,949 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:39:46,949 - root - INFO - Clearing user context +2025-12-04 14:39:46,952 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:46,952 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:46,953 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:46,953 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:46,955 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:46,956 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:46,956 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:46,958 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:46,958 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:46,959 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:46,961 - root - INFO - Clearing user context +2025-12-04 14:39:48,856 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:39:48,856 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:48,856 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:39:48,857 - root - INFO - Clearing user context +2025-12-04 14:39:48,858 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:48,858 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:48,859 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:48,859 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:48,860 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:48,860 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:48,861 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:48,862 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:48,862 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:48,862 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:48,868 - root - INFO - Clearing user context +2025-12-04 14:39:49,685 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:39:49,685 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:49,685 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:39:49,685 - root - INFO - Clearing user context +2025-12-04 14:39:49,687 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:49,687 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:49,687 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:49,688 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:49,689 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:49,689 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:49,689 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:49,690 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:49,691 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:49,691 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:49,699 - root - INFO - Clearing user context +2025-12-04 14:39:49,711 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:39:49,711 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:49,711 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:39:49,712 - root - INFO - Clearing user context +2025-12-04 14:39:49,713 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:49,713 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:49,714 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:49,714 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:49,715 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:49,716 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:49,716 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:49,718 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:49,719 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:49,719 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:49,723 - root - INFO - Clearing user context +2025-12-04 14:39:49,732 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/4 +2025-12-04 14:39:49,732 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:49,733 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:39:49,733 - root - INFO - Clearing user context +2025-12-04 14:39:49,735 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:49,736 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:49,736 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:49,736 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:49,738 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:49,738 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:49,739 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:49,740 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:49,741 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:49,741 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:49,743 - root - INFO - Clearing user context +2025-12-04 14:39:54,932 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:39:54,932 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:39:54,933 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:39:54,933 - root - INFO - Clearing user context +2025-12-04 14:39:54,935 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:39:54,936 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:54,936 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:39:54,936 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:39:54,938 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:39:54,939 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:39:54,939 - root - INFO - Looking for user with username: admin +2025-12-04 14:39:54,940 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:39:54,941 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:39:54,941 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:39:54,947 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:39:54,948 - root - INFO - Clearing user context +2025-12-04 14:40:03,539 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:40:03,539 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:03,540 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:40:03,540 - root - INFO - Clearing user context +2025-12-04 14:40:03,541 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:03,541 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:03,542 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:03,542 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:03,543 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:03,544 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:03,544 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:03,545 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:03,545 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:03,545 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:03,553 - th_agenter.workflow_api - INFO - Updated workflow: qw - 副本 by user admin +2025-12-04 14:40:03,554 - root - INFO - Clearing user context +2025-12-04 14:40:11,962 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:40:11,963 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:11,963 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:40:11,963 - root - INFO - Clearing user context +2025-12-04 14:40:11,964 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:11,965 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:11,965 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:11,965 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:11,967 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:11,967 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:11,967 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:11,968 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:11,968 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:11,968 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:11,970 - root - ERROR - Database session error: [{'type': 'string_too_short', 'loc': ('body', 'definition', 'nodes', 1, 'parameters', 'outputs', 0, 'name'), 'msg': 'String should have at least 1 character', 'input': '', 'ctx': {'min_length': 1}}] +2025-12-04 14:40:11,971 - root - INFO - Clearing user context +2025-12-04 14:40:17,596 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:40:17,597 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:17,597 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:40:17,597 - root - INFO - Clearing user context +2025-12-04 14:40:17,598 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:17,599 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:17,599 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:17,599 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:17,601 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:17,601 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:17,601 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:17,602 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:17,602 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:17,603 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:17,604 - root - ERROR - Database session error: [{'type': 'string_too_short', 'loc': ('body', 'definition', 'nodes', 1, 'parameters', 'outputs', 0, 'name'), 'msg': 'String should have at least 1 character', 'input': '', 'ctx': {'min_length': 1}}] +2025-12-04 14:40:17,604 - root - INFO - Clearing user context +2025-12-04 14:40:20,249 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:40:20,249 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:20,250 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:40:20,250 - root - INFO - Clearing user context +2025-12-04 14:40:20,251 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:20,251 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:20,251 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:20,252 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:20,253 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:20,253 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:20,254 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:20,255 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:20,255 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:20,255 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:20,256 - root - ERROR - Database session error: [{'type': 'string_too_short', 'loc': ('body', 'definition', 'nodes', 1, 'parameters', 'outputs', 0, 'name'), 'msg': 'String should have at least 1 character', 'input': '', 'ctx': {'min_length': 1}}] +2025-12-04 14:40:20,256 - root - INFO - Clearing user context +2025-12-04 14:40:23,533 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/4 +2025-12-04 14:40:23,533 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/4 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:23,533 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/4 +2025-12-04 14:40:23,533 - root - INFO - Clearing user context +2025-12-04 14:40:23,535 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:23,544 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:23,545 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:23,545 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:23,547 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:23,547 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:23,547 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:23,548 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:23,549 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:23,549 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:23,550 - root - ERROR - Database session error: [{'type': 'string_too_short', 'loc': ('body', 'definition', 'nodes', 1, 'parameters', 'outputs', 0, 'name'), 'msg': 'String should have at least 1 character', 'input': '', 'ctx': {'min_length': 1}}] +2025-12-04 14:40:23,551 - root - INFO - Clearing user context +2025-12-04 14:40:24,440 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:40:24,440 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:24,440 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:40:24,441 - root - INFO - Clearing user context +2025-12-04 14:40:24,442 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:24,443 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:24,443 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:24,443 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:24,444 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:24,445 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:24,445 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:24,446 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:24,447 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:24,447 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:24,456 - root - INFO - Clearing user context +2025-12-04 14:40:25,392 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/ +2025-12-04 14:40:25,392 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:25,393 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/ +2025-12-04 14:40:25,393 - root - INFO - Clearing user context +2025-12-04 14:40:25,394 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:25,395 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:25,395 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:25,395 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:25,397 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:25,397 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:25,398 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:25,399 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:25,399 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:25,400 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:25,405 - root - INFO - Clearing user context +2025-12-04 14:40:25,415 - root - INFO - [MIDDLEWARE] Processing request: GET /api/admin/llm-configs/ +2025-12-04 14:40:25,416 - root - INFO - [MIDDLEWARE] Checking path: /api/admin/llm-configs/ against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:25,416 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/admin/llm-configs/ +2025-12-04 14:40:25,416 - root - INFO - Clearing user context +2025-12-04 14:40:25,419 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:25,419 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:25,419 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:25,419 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:25,421 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:25,421 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:25,421 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:25,423 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:25,423 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:25,423 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:25,425 - root - INFO - Clearing user context +2025-12-04 14:40:25,436 - root - INFO - [MIDDLEWARE] Processing request: GET /api/workflows/3 +2025-12-04 14:40:25,436 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:25,437 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:40:25,437 - root - INFO - Clearing user context +2025-12-04 14:40:25,439 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:25,439 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:25,440 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:25,440 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:25,441 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:25,441 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:25,442 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:25,443 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:25,443 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:25,443 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:25,445 - root - INFO - Clearing user context +2025-12-04 14:40:37,160 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:40:37,160 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:37,160 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:40:37,160 - root - INFO - Clearing user context +2025-12-04 14:40:37,162 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:37,162 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:37,162 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:37,163 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:37,164 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:37,164 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:37,165 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:37,166 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:37,166 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:37,166 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:37,167 - root - ERROR - Database session error: [{'type': 'string_too_short', 'loc': ('body', 'definition', 'nodes', 1, 'parameters', 'outputs', 0, 'name'), 'msg': 'String should have at least 1 character', 'input': '', 'ctx': {'min_length': 1}}] +2025-12-04 14:40:37,167 - root - INFO - Clearing user context +2025-12-04 14:40:40,585 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:40:40,586 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:40,586 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:40:40,586 - root - INFO - Clearing user context +2025-12-04 14:40:40,587 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:40,588 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:40,588 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:40,588 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:40,590 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:40,590 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:40,590 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:40,591 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:40,592 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:40,592 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:40,601 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:40:40,603 - root - INFO - Clearing user context +2025-12-04 14:40:45,309 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:40:45,309 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:45,310 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:40:45,310 - root - INFO - Clearing user context +2025-12-04 14:40:45,311 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:45,311 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:45,312 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:45,312 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:45,313 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:45,314 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:45,314 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:45,315 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:45,315 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:45,315 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:45,323 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:40:45,324 - root - INFO - Clearing user context +2025-12-04 14:40:51,648 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:40:51,648 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:51,649 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:40:51,649 - root - INFO - Clearing user context +2025-12-04 14:40:51,650 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:51,650 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:51,650 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:51,651 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:51,652 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:51,653 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:51,653 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:51,654 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:51,654 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:51,654 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:51,669 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:40:51,670 - root - INFO - Clearing user context +2025-12-04 14:40:57,629 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:40:57,629 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:40:57,630 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:40:57,630 - root - INFO - Clearing user context +2025-12-04 14:40:57,631 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:40:57,631 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:57,632 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:40:57,632 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:40:57,633 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:40:57,634 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:40:57,634 - root - INFO - Looking for user with username: admin +2025-12-04 14:40:57,635 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:40:57,635 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:40:57,635 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:40:57,651 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:40:57,652 - root - INFO - Clearing user context +2025-12-04 14:41:32,811 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:41:32,811 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:41:32,812 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:41:32,812 - root - INFO - Clearing user context +2025-12-04 14:41:32,813 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:41:32,813 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:41:32,814 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:41:32,814 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:41:32,815 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:41:32,816 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:41:32,816 - root - INFO - Looking for user with username: admin +2025-12-04 14:41:32,817 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:41:32,817 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:41:32,818 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:41:32,825 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:41:32,826 - root - INFO - Clearing user context +2025-12-04 14:41:35,305 - root - INFO - [MIDDLEWARE] Processing request: PUT /api/workflows/3 +2025-12-04 14:41:35,305 - root - INFO - [MIDDLEWARE] Checking path: /api/workflows/3 against exclude_paths: ['/docs', '/redoc', '/openapi.json', '/api/auth/login', '/api/auth/register', '/api/auth/login-oauth', '/auth/login', '/auth/register', '/auth/login-oauth', '/health', '/test'] +2025-12-04 14:41:35,305 - root - INFO - [MIDDLEWARE] Processing authenticated request: /api/workflows/3 +2025-12-04 14:41:35,306 - root - INFO - Clearing user context +2025-12-04 14:41:35,307 - root - INFO - Setting user in context with token: admin (ID: 2) +2025-12-04 14:41:35,307 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:41:35,308 - root - INFO - User admin (ID: 2) authenticated and set in context +2025-12-04 14:41:35,308 - root - INFO - Verified current user ID in context: 2 +2025-12-04 14:41:35,309 - root - INFO - Received token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJhZ... +2025-12-04 14:41:35,310 - root - INFO - Token payload: {'sub': 'admin', 'exp': 1764843655} +2025-12-04 14:41:35,310 - root - INFO - Looking for user with username: admin +2025-12-04 14:41:35,311 - root - INFO - Setting user in context: admin (ID: 2) +2025-12-04 14:41:35,311 - root - INFO - Verification - ContextVar user: admin +2025-12-04 14:41:35,311 - root - INFO - User admin (ID: 2) set in UserContext +2025-12-04 14:41:35,326 - th_agenter.workflow_api - INFO - Updated workflow: qw -eee233444 by user admin +2025-12-04 14:41:35,327 - root - INFO - Clearing user context +2025-12-04 14:42:15,152 - root - INFO - Shutting down TH-Agenter application... diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..61638b7 --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,77 @@ +# Web框架和核心依赖 +fastapi>=0.104.1 +uvicorn[standard]>=0.24.0 +pydantic>=2.5.0 +sqlalchemy>=2.0.23 +alembic>=1.13.1 +python-multipart>=0.0.6 +python-jose[cryptography]>=3.3.0 +passlib[bcrypt]>=1.7.4 +python-dotenv>=1.0.0 + +# 数据库和向量数据库 +psycopg2-binary>=2.9.7 # PostgreSQL +pgvector>=0.2.4 # PostgreSQL pgvector extension +pymysql>=1.1.2 #mysql + +# Excel和数据分析(智能问数功能) +pandas>=2.1.0 +numpy>=1.24.0 +openpyxl>=3.1.0 # Excel文件读写 +xlrd>=2.0.1 # 旧版Excel文件支持 + +# LangChain AI框架 +langchain>=0.1.0 +langchain-community>=0.0.10 +langchain-experimental>=0.0.50 # pandas代理 +langchain-postgres>=0.0.6 # PGVector支持 +langchain-openai>=0.0.5 # OpenAI集成 +langgraph>=0.0.40 # LangGraph工作流编排 + +# AI模型服务商 +zhipuai>=2.0.0 # 智谱AI +openai>=1.0.0 # OpenAI + +# 文档处理(知识库功能) +pypdf2>=3.0.0 # PDF文件处理 +python-docx>=0.8.11 # Word文档处理 +markdown>=3.5.0 # Markdown文件处理 +chardet>=5.2.0 # 文件编码检测 +pdfplumber>=0.11.7 #pdf内容提取 + +# 工作流编排和智能体 +celery>=5.3.0 # 异步任务队列 +redis>=5.0.0 # Redis缓存和消息队列 +apscheduler>=3.10.0 # 定时任务调度 + +# 文件和网络处理 +aiofiles>=23.2.0 # 异步文件操作 +requests>=2.31.0 +httpx>=0.25.0 +pyyaml>=6.0 # YAML配置文件解析 +boto3>=1.40.30 #云对象存储 + +# 开发和测试工具 +pytest>=7.4.0 +pytest-asyncio>=0.21.0 +pytest-cov>=4.1.0 +black>=23.0.0 +isort>=5.12.0 +flake8>=6.0.0 +mypy>=1.5.0 +pre-commit>=3.3.0 + +# 数据库迁移 +alembic>=1.12.0 + +# 监控和日志 +prometheus-client>=0.17.0 +structlog>=23.1.0 + +# 安全 +cryptography>=41.0.0 +passlib[bcrypt]>=1.7.4 +python-jose[cryptography]>=3.3.0 + +# 性能优化 +orjson>=3.9.0 \ No newline at end of file diff --git a/backend/tests/fastapi_test/main.py b/backend/tests/fastapi_test/main.py new file mode 100644 index 0000000..65fd6f9 --- /dev/null +++ b/backend/tests/fastapi_test/main.py @@ -0,0 +1,153 @@ +from contextvars import ContextVar +from fastapi import FastAPI, Request, Depends, HTTPException +from fastapi.security import OAuth2PasswordBearer +from pydantic import BaseModel +from typing import Optional +import uuid +import uvicorn + +app = FastAPI() +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token") + +# 创建上下文变量存储当前用户和请求ID +current_user_ctx: ContextVar[dict] = ContextVar("current_user", default=None) +request_id_ctx: ContextVar[str] = ContextVar("request_id", default=None) + + +# 用户模型 +class User(BaseModel): + id: int + username: str + email: Optional[str] = None + + +# 模拟用户服务 +class UserService: + @staticmethod + def get_current_user_id() -> int: + """在service中直接获取当前用户ID""" + user = current_user_ctx.get() + if not user: + raise RuntimeError("No current user available") + return user["id"] + + @staticmethod + def get_current_user() -> dict: + """获取完整的当前用户信息""" + user = current_user_ctx.get() + if not user: + raise RuntimeError("No current user available") + return user + + +# 业务服务示例 +class TaskService: + def create_task(self, task_data: dict): + """创建任务时自动添加当前用户ID""" + current_user_id = UserService.get_current_user_id() + + # 这里模拟数据库操作 + task = { + **task_data, + "created_by": current_user_id, + "created_at": "2023-10-01 12:00:00" + } + + print(f"Task created by user {current_user_id}: {task}") + return task + + def get_user_tasks(self): + """获取当前用户的任务""" + user = current_user_ctx.get() + current_user_id = UserService.get_current_user_id() + + # 模拟根据用户ID查询任务 + return [{"id": 1, "title": "Sample task", "user_id": current_user_id}] + + +# 中间件:设置上下文 +@app.middleware("http") +async def set_context_vars(request: Request, call_next): + # 为每个请求生成唯一ID + request_id = str(uuid.uuid4()) + request_id_token = request_id_ctx.set(request_id) + + # 尝试提取用户信息 + user_token = None + try: + auth_header = request.headers.get("Authorization") + if auth_header and auth_header.startswith("Bearer "): + token = auth_header.replace("Bearer ", "") + user = await decode_token_and_get_user(token) # 您的认证逻辑 + user_token = current_user_ctx.set(user) + + response = await call_next(request) + return response + finally: + # 清理上下文 + request_id_ctx.reset(request_id_token) + if user_token: + current_user_ctx.reset(user_token) + + +# 模拟认证函数 +async def decode_token_and_get_user(token: str) -> dict: + # 这里应该是您的实际认证逻辑,例如JWT解码或数据库查询 + # 简单模拟:根据token返回用户信息 + if token == "valid_token_123": + return {"id": 123, "username": "john_doe", "email": "john@example.com"} + elif token == "valid_token_456": + return {"id": 456, "username": "jane_doe", "email": "jane@example.com"} + else: + return None + + +# 依赖项:用于路由层认证 +async def get_current_user_route(token: str = Depends(oauth2_scheme)) -> dict: + """路由层的用户认证""" + user = await decode_token_and_get_user(token) + if not user: + raise HTTPException(status_code=401, detail="Invalid credentials") + return user + + +# 路由处理函数 +@app.post("/tasks") +async def create_task( + task_data: dict, + current_user: dict = Depends(get_current_user_route) +): + """创建任务""" + # 不需要显式传递user_id到service! + task_service = TaskService() + task = task_service.create_task(task_data) + return {"task": task, "message": "Task created successfully"} + + +@app.get("/tasks") +async def get_tasks(current_user: dict = Depends(get_current_user_route)): + """获取当前用户的任务""" + task_service = TaskService() + tasks = task_service.get_user_tasks() + return {"tasks": tasks} + + +@app.get("/users/me") +async def read_users_me(current_user: dict = Depends(get_current_user_route)): + """获取当前用户信息""" + return current_user + + +# 测试端点 - 直接在路由中获取上下文用户 +@app.get("/test-context") +async def test_context(): + """测试直接通过上下文获取用户(不通过依赖注入)""" + try: + user = UserService.get_current_user() + return {"message": "Successfully got user from context", "user": user} + except RuntimeError as e: + return {"error": str(e)} + + +if __name__ == "__main__": + uvicorn.run(app, host="0.0.0.0", port=8000) \ No newline at end of file diff --git a/backend/tests/fastapi_test/test_main.py b/backend/tests/fastapi_test/test_main.py new file mode 100644 index 0000000..e206c62 --- /dev/null +++ b/backend/tests/fastapi_test/test_main.py @@ -0,0 +1,93 @@ +import pytest +from fastapi.testclient import TestClient +from main import app, current_user_ctx, UserService +from contextvars import ContextVar + +client = TestClient(app) + + +def test_read_users_me_with_valid_token(): + """测试有效令牌获取用户信息""" + response = client.get( + "/users/me", + headers={"Authorization": "Bearer valid_token_123"} + ) + assert response.status_code == 200 + assert response.json()["id"] == 123 + assert response.json()["username"] == "john_doe" + + +def test_read_users_me_with_invalid_token(): + """测试无效令牌""" + response = client.get( + "/users/me", + headers={"Authorization": "Bearer invalid_token"} + ) + assert response.status_code == 401 + assert response.json()["detail"] == "Invalid credentials" + + +def test_create_task_with_user_context(): + """测试创建任务时用户上下文是否正确""" + response = client.post( + "/tasks", + json={"title": "Test task", "description": "Test description"}, + headers={"Authorization": "Bearer valid_token_123"} + ) + assert response.status_code == 200 + # 检查响应中是否包含正确的用户ID + assert response.json()["task"]["created_by"] == 123 + + +def test_get_tasks_with_different_users(): + """测试不同用户获取任务""" + # 用户1 + response1 = client.get( + "/tasks", + headers={"Authorization": "Bearer valid_token_123"} + ) + assert response1.status_code == 200 + # 这里应该只返回用户1的任务 + + # 用户2 + response2 = client.get( + "/tasks", + headers={"Authorization": "Bearer valid_token_456"} + ) + assert response2.status_code == 200 + # 这里应该只返回用户2的任务 + + +def test_context_outside_request(): + """测试在请求上下文外获取用户(应该失败)""" + try: + UserService.get_current_user() + assert False, "Should have raised an exception" + except RuntimeError as e: + assert "No current user available" in str(e) + + +# 手动设置上下文进行测试 +def test_user_service_with_manual_context(): + """测试手动设置上下文后获取用户""" + test_user = {"id": 999, "username": "test_user"} + token = current_user_ctx.set(test_user) + + try: + user_id = UserService.get_current_user_id() + assert user_id == 999 + + user = UserService.get_current_user() + assert user["username"] == "test_user" + finally: + current_user_ctx.reset(token) + + +if __name__ == "__main__": + # pytest.main([__file__, "-v"]) + test_read_users_me_with_valid_token() + test_read_users_me_with_invalid_token() + test_create_task_with_user_context() + test_get_tasks_with_different_users() + test_context_outside_request() + test_user_service_with_manual_context() diff --git a/backend/tests/init_db.py b/backend/tests/init_db.py new file mode 100644 index 0000000..2554f75 --- /dev/null +++ b/backend/tests/init_db.py @@ -0,0 +1,100 @@ +#!/usr/bin/env python3 +"""init db""" + +import sys +import os + + +def find_project_root(): + """智能查找项目根目录""" + current_dir = os.path.abspath(os.getcwd()) + script_dir = os.path.dirname(os.path.abspath(__file__)) + + # 可能的项目根目录位置 + possible_roots = [ + current_dir, # 当前工作目录 + script_dir, # 脚本所在目录 + os.path.dirname(script_dir), # 脚本父目录 + os.path.dirname(os.path.dirname(script_dir)) # 脚本祖父目录 + ] + + for root in possible_roots: + backend_dir = os.path.join(root, 'backend') + if os.path.exists(backend_dir) and os.path.exists(os.path.join(backend_dir, 'th_agenter')): + return root, backend_dir + + raise FileNotFoundError("无法找到项目根目录和backend目录") + + +# 查找项目根目录和backend目录 +project_root, backend_dir = find_project_root() + +# 添加backend目录到Python路径 +sys.path.insert(0, backend_dir) + +# 保存原始工作目录 +original_cwd = os.getcwd() + +# 设置工作目录为backend,以便找到.env文件 +os.chdir(backend_dir) + +from th_agenter.db.database import get_db, init_db +from th_agenter.services.user import UserService +from th_agenter.utils.schemas import UserCreate +import asyncio + + +async def create_database_tables(): + """Create all database tables using SQLAlchemy models.""" + try: + await init_db() + print('Database tables created successfully using SQLAlchemy models') + return True + except Exception as e: + print(f'Error creating database tables: {e}') + return False + + +async def create_test_user(): + """Create a test user.""" + # First, create all database tables using SQLAlchemy models + if not await create_database_tables(): + print('Failed to create database tables') + return None + + db = next(get_db()) + + try: + user_service = UserService(db) + + # Create test user + user_data = UserCreate( + username='test', + email='test@example.com', + password='123456', + full_name='Test User 1' + ) + + # Check if user already exists + existing_user = user_service.get_user_by_email(user_data.email) + if existing_user: + print(f'User already exists: {existing_user.username} ({existing_user.email})') + return existing_user + + # Create new user + user = user_service.create_user(user_data) + print(f'Created user: {user.username} ({user.email})') + return user + except Exception as e: + print(f'Error creating user: {e}') + return None + finally: + db.close() + + +if __name__ == "__main__": + try: + asyncio.run(create_test_user()) + finally: + # 恢复原始工作目录 + os.chdir(original_cwd) \ No newline at end of file diff --git a/backend/tests/pandas_test.py b/backend/tests/pandas_test.py new file mode 100644 index 0000000..0dbf552 --- /dev/null +++ b/backend/tests/pandas_test.py @@ -0,0 +1,125 @@ +import os +import sys +import asyncio +import pandas as pd +import tempfile +import pickle +from datetime import datetime +from typing import Dict, Any, List + +sys.path.insert(0,os.path.join(os.path.dirname(__file__),'..','','backend')) + + +def execute(df_1,df_2): + # 假设合同日期列是字符串类型,将其转换为日期类型 + if '合同日期' in df_1.columns: + df_1['合同日期'] = pd.to_datetime(df_1['合同日期']) + if '合同日期' in df_2.columns: + df_2['合同日期'] = pd.to_datetime(df_2['合同日期']) + + # 筛选出2024年和2025年的数据 + filtered_df_1 = df_1[ + (df_1['合同日期'].dt.year == 2024) | (df_1['合同日期'].dt.year == 2025)] + filtered_df_2 = df_2[ + (df_2['合同日期'].dt.year == 2024) | (df_2['合同日期'].dt.year == 2025)] + # 合并两个数据框 + combined_df = pd.concat([filtered_df_1[:5], filtered_df_2[:7]], ignore_index=True) + # 在去重前清理空值 + # combined_df_clean = combined_df.dropna(subset=['项目号']) # 确保主键不为空 + + # 填充数值列的空值 + combined_df_filled = combined_df.fillna({ + '总合同额': 0, + '已确认比例': 0, + '分包合同额': 0 + }) + # 找出不同的项目 + unique_projects = combined_df.drop_duplicates(subset=['项目号']) + return unique_projects + + + +def test_load_selected_dataframes(): + + try: + file1_path = '2025年在手合同数据.xlsx.pkl' + file2_path = '2024年在手合同数据.xlsx.pkl' + target_filenames = [file1_path,file2_path] + dataframes = {} + base_dir = os.path.join("D:\workspace-py\chat-agent\\backend","data","uploads","excel_6") + + all_files = os.listdir(base_dir) + for filename in target_filenames: + matching_files = [] + for file in all_files: + if file.endswith(f"_{filename}") or file.endswith(f"_{filename}.pkl"): + matching_files.append(file) + if not matching_files: + print(f"未找到匹配的文件: {filename}") + + # 如果有多个匹配文件,选择最新的 + if len(matching_files) > 1: + matching_files.sort(key=lambda x: os.path.getmtime(os.path.join(base_dir, x)), reverse=True) + print(f"找到多个匹配文件,选择最新的: {matching_files[0]}") + continue + + selected_file = matching_files[0] + file_path = os.path.join(base_dir, selected_file) + + try: + # 优先加载pickle文件 + if selected_file.endswith('.pkl'): + with open(file_path, 'rb') as f: + df = pickle.load(f) + print(f"成功从pickle加载文件: {selected_file}") + else: + # 如果没有pickle文件,尝试加载原始文件 + if selected_file.endswith(('.xlsx', '.xls')): + df = pd.read_excel(file_path) + elif selected_file.endswith('.csv'): + df = pd.read_csv(file_path) + else: + print(f"不支持的文件格式: {selected_file}") + continue + print(f"成功从原始文件加载: {selected_file}") + + # 使用原始文件名作为key + dataframes[filename] = df + print(f"成功加载DataFrame: {filename}, 形状: {df.shape}") + + except Exception as e: + print(f"加载文件失败 {selected_file}: {e}") + continue + + return dataframes + except Exception as e: + print(e) + +if __name__ == '__main__': + dataframes = test_load_selected_dataframes() + df_names = list(dataframes.keys()) + if len(df_names) >= 2: + df_1 = dataframes[df_names[0]] + df_2 = dataframes[df_names[1]] + + print(f"DataFrame 1 ({df_names[0]}) 形状: {df_1.shape}") + print(f"DataFrame 1 列名: {list(df_1.columns)}") + print(f"DataFrame 1 前几行:") + print(df_1.head()) + print() + + print(f"DataFrame 2 ({df_names[1]}) 形状: {df_2.shape}") + print(f"DataFrame 2 列名: {list(df_2.columns)}") + print(f"DataFrame 2 前几行:") + print(df_2.head()) + print() + + # 执行用户提供的数据处理逻辑 + print("执行数据处理逻辑...") + result = execute(df_1, df_2) + + print("处理结果:") + print(f"结果形状: {result.shape}") + print(f"结果列名: {list(result.columns)}") + print("结果数据:") + print(result) diff --git a/backend/th_agenter/__init__.py b/backend/th_agenter/__init__.py new file mode 100644 index 0000000..18c0364 --- /dev/null +++ b/backend/th_agenter/__init__.py @@ -0,0 +1,12 @@ +"""th - A modern chat agent application.""" + +__version__ = "0.1.0" +__author__ = "Your Name" +__email__ = "your.email@example.com" +__description__ = "A modern chat agent application with Vue frontend and FastAPI backend" + +# 导出主要组件 +from .core.config import settings +from .core.app import create_app + +__all__ = ["settings", "create_app", "__version__"] \ No newline at end of file diff --git a/backend/th_agenter/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1cddd672035f2041a6393562ec6a1bb5d9ff5dee GIT binary patch literal 545 zcmah_O=}cE5bfU2nOQdxLGjp#9tPQQgBMW=?jeE+gvfvpSSZ@r&Q9Y@_fXSok~wW| z`2+b8{-bq35IuQPSiGlZP0luS@%nY~s@_`P+TErlQ2=)!u)!#Hpr{ z9~MfSpfygJnUHkf%F<1a-iePg`9$0=^29m%-}JkLlC)xIOJ(u;+o>8w!EZhrMOw4`|Po?K)dB8SXjfJLi1o+)v1- znx-Il)V1eZFL;FhV!`B*#?I+t0-+y~iA>Hw>m1=q%ug5zl87x&c(A-7tj~~{60$^7 zSos1W=W~RtnR#SMOWcU5mYq;f?L`<#S_G7UhILLxp}lS3Lt{p%oy5wnxr9M{?rF0M#fv!-UI&CIg!zcpDiXXeeqn5A-l&g6EaXk}}Q z=kuU<#wym{JD&mcgA36Q&!gsRY{zDznb2qdW@(+HYRh*TcEgXbw{r(#VZ({GsBGKY zj_cVrAWEHJC&IQ91f9!%fV~?{>_w$docfp8Ym`h(b}(T~4II{q8$_<}b;L?#%MYWD zv{;RQoy;u=OOapNbfGEo^#qj~_^#7#MO4_rt$?)zK8f6q-$baB*#P%!IZuUXFTg~G zwMi0=j$Ek_%(kaW82JR-5ozO2{-)<_ws6U5NB%euqeQ39)`}Z-pkljGfsLc$h(w>G z;z$JoUmBtbKcsxP7g8wzL0ax65f}`@c+NJqZRXu|Tg(#!hkznf-5g0`H(Ua}5rGLz zOwsj143i2>JlkRFRqL>D6%-{RW97l4QhUb**rT#Tnm`$1n!Z7r?QN*vU?GVoF`IH4 z4I8TB9OYdYuEZS-45<*GJZXDG1A3BXM+T0^=oC&wS8uLZuij*Zz3Kmk^M96-iYhG2#whIB?vrYb4N08fS7{FKt1RE4zs zB83nSUB~p)ypq%+JwHvxTkP@iDf!9q@hcfVgABN3?`jp3UyzyyR->Dnn3)$-lA)Uj z(GioBn4F!OmjV=uhY2MXWX8nDXXa&=#K-FuRNmsSfmmi�E4Ewji#{+jGBVy` JP$*&nasaebHgW&} literal 0 HcmV?d00001 diff --git a/backend/th_agenter/api/__pycache__/routes.cpython-313.pyc b/backend/th_agenter/api/__pycache__/routes.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7fdacfd473f02e3c9ca763e756077707cc598509 GIT binary patch literal 2152 zcmah~&uimG6dqZ>C0lmvwH@0@)=72?hIQOSm$Ic40xhMR?QVBEcON5q!ABbju4 z+O&rldhUPFOaB|29_$JN3ni4?62cyO>zk3Ch^8#Eju+KTnQOpD8F;B4oSil0sB481V z6ia|5EKw{2ma$B+0$9Nc#VTMGs}v^zCvlSE6yOw2QLF*huvSIQ#14A8C=;if>BH@X z6WN;l;a0?5Ks~Kz8jQa7jD;-mIxbe1)pIL!LagqAo?oF8V|4{k6`#R<-&3U&t6E>7 zl4Dh+6)GiGRbHV|V^tM>19bm)|H*rFn=5pwSlyQXaD`5b)m8QFh;BD#l6IalVN~BO z-%Vq-`>6JKl=g)3LiG0|a`O(YJh)JcJ03-Ja!< zRJ~q*K_o6e^ejw(bmHRUPR~6V@eNW(pb=|1zTGl?+jVM*Kp0@FWB(X%zS;GFM$X4P z^f1aaZcDC{O1x3}<3QL#ebEBW5?RHdyzfym*9g8gs4Xu1Q-+3&&y*84CCAG@#3*0JTZ zY1n|Aj^8u~c9YJEw>Nkjq)2`PCnHchyfK_6W#H5HJ_lVPA z0wJ8_K(n1zZ`ihsh0k`aut0hPxJU%+L?C_HciY3B^%bsyOQzlX8|)R!FwCE*aDg_@ zQRRb#n2mGv_#Ew=qwNdy3E&R`V&EABHVCXi>hS2yQMWg>4xXvqjIJ>v>ft}s`M zh#rIEzXtvcr(J$P(nH@pO(kp{=q!BPB6^g M4T}^M96-iYhG2#whIB?vrYc#-08fR~yp)3c%)F9f zg|z%4g%A&2$Mn>^lGGwSKTXD4?D6p_`N{F|D;Yk6%(&(0Y88`TkeUZpqnn$UnHN)% zp_>TN5tEddoDDP%C=?G9N-W5Xf!h!hAD@|*SrQ+wS5SG2!zMRBr8Fniu80k2GRTR= TAm@ExW@Kc%#h_fo0^|SyEa*G~ literal 0 HcmV?d00001 diff --git a/backend/th_agenter/api/endpoints/__pycache__/auth.cpython-313.pyc b/backend/th_agenter/api/endpoints/__pycache__/auth.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a5180d6768d373bc97152bd51bbd5d22354ea461 GIT binary patch literal 5117 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 literal 0 HcmV?d00001 diff --git a/backend/th_agenter/api/endpoints/__pycache__/llm_configs.cpython-313.pyc b/backend/th_agenter/api/endpoints/__pycache__/llm_configs.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e15041015f108470841f7d741107c19bc13800da GIT binary patch literal 22527 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 JC0~W@@IQfdFn$04 literal 0 HcmV?d00001 diff --git a/backend/th_agenter/api/endpoints/__pycache__/smart_chat.cpython-313.pyc b/backend/th_agenter/api/endpoints/__pycache__/smart_chat.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0c80152ceb777f6e077daea71950f5b2c684c36d GIT binary patch literal 13829 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 xb4@9{-J%dh-nlr1-DY{~yP*BfS6s literal 0 HcmV?d00001 diff --git a/backend/th_agenter/api/endpoints/__pycache__/smart_query.cpython-313.pyc b/backend/th_agenter/api/endpoints/__pycache__/smart_query.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..23b62e051fa621c12e1128c57de0cc902886ba73 GIT binary patch literal 30956 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�pq%$adW`9`IOsl+*XWtE3GDMt6YR2tKm!BBCT0 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 literal 0 HcmV?d00001 diff --git a/backend/th_agenter/api/endpoints/__pycache__/table_metadata.cpython-313.pyc b/backend/th_agenter/api/endpoints/__pycache__/table_metadata.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cbb952586a4810ff7dbe53992de226dccf2c69d1 GIT binary patch literal 11424 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 literal 0 HcmV?d00001 diff --git a/backend/th_agenter/api/endpoints/__pycache__/users.cpython-313.pyc b/backend/th_agenter/api/endpoints/__pycache__/users.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9c8e85ae6572b2c0f85ea7c5aaa1208582444cb0 GIT binary patch literal 9101 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 S-uf7ULN1ZV98g37U-=)s0ZmK* literal 0 HcmV?d00001 diff --git a/backend/th_agenter/api/endpoints/__pycache__/workflow.cpython-313.pyc b/backend/th_agenter/api/endpoints/__pycache__/workflow.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1c142645ee932b6467037129e126f533191be191 GIT binary patch literal 23005 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 literal 0 HcmV?d00001 diff --git a/backend/th_agenter/api/endpoints/auth.py b/backend/th_agenter/api/endpoints/auth.py new file mode 100644 index 0000000..f063d7a --- /dev/null +++ b/backend/th_agenter/api/endpoints/auth.py @@ -0,0 +1,125 @@ +"""Authentication endpoints.""" + +from datetime import timedelta +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import OAuth2PasswordRequestForm +from sqlalchemy.orm import Session + +from ...core.config import get_settings +from ...db.database import get_db +from ...services.auth import AuthService +from ...services.user import UserService +from ...schemas.user import UserResponse, UserCreate +from ...utils.schemas import Token, LoginRequest + +router = APIRouter() +settings = get_settings() + + +@router.post("/register", response_model=UserResponse) +async def register( + user_data: UserCreate, + db: Session = Depends(get_db) +): + """Register a new user.""" + user_service = UserService(db) + + # Check if user already exists + if user_service.get_user_by_email(user_data.email): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Email already registered" + ) + + if user_service.get_user_by_username(user_data.username): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Username already taken" + ) + + # Create user + user = user_service.create_user(user_data) + return UserResponse.from_orm(user) + + +@router.post("/login", response_model=Token) +async def login( + login_data: LoginRequest, + db: Session = Depends(get_db) +): + """Login with email and password.""" + # Authenticate user by email + user = AuthService.authenticate_user_by_email(db, login_data.email, login_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Create access token + access_token_expires = timedelta(minutes=settings.security.access_token_expire_minutes) + access_token = AuthService.create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + + return { + "access_token": access_token, + "token_type": "bearer", + "expires_in": settings.security.access_token_expire_minutes * 60 + } + + +@router.post("/login-oauth", response_model=Token) +async def login_oauth( + form_data: OAuth2PasswordRequestForm = Depends(), + db: Session = Depends(get_db) +): + """Login with username and password (OAuth2 compatible).""" + # Authenticate user + user = AuthService.authenticate_user(db, form_data.username, form_data.password) + if not user: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect username or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + + # Create access token + access_token_expires = timedelta(minutes=settings.security.access_token_expire_minutes) + access_token = AuthService.create_access_token( + data={"sub": user.username}, expires_delta=access_token_expires + ) + + return { + "access_token": access_token, + "token_type": "bearer", + "expires_in": settings.security.access_token_expire_minutes * 60 + } + + +@router.post("/refresh", response_model=Token) +async def refresh_token( + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """Refresh access token.""" + # Create new access token + access_token_expires = timedelta(minutes=settings.security.access_token_expire_minutes) + access_token = AuthService.create_access_token( + data={"sub": current_user.username}, expires_delta=access_token_expires + ) + + return { + "access_token": access_token, + "token_type": "bearer", + "expires_in": settings.security.access_token_expire_minutes * 60 + } + + +@router.get("/me", response_model=UserResponse) +async def get_current_user_info( + current_user = Depends(AuthService.get_current_user) +): + """Get current user information.""" + return UserResponse.from_orm(current_user) \ No newline at end of file diff --git a/backend/th_agenter/api/endpoints/chat.py b/backend/th_agenter/api/endpoints/chat.py new file mode 100644 index 0000000..cc51746 --- /dev/null +++ b/backend/th_agenter/api/endpoints/chat.py @@ -0,0 +1,237 @@ +"""Chat endpoints.""" + +from typing import List +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.responses import StreamingResponse +from sqlalchemy.orm import Session + +from ...db.database import get_db +from ...models.user import User +from ...services.auth import AuthService +from ...services.chat import ChatService +from ...services.conversation import ConversationService +from ...utils.schemas import ( + ConversationCreate, + ConversationResponse, + ConversationUpdate, + MessageCreate, + MessageResponse, + ChatRequest, + ChatResponse +) + +router = APIRouter() + + +# Conversation management +@router.post("/conversations", response_model=ConversationResponse) +async def create_conversation( + conversation_data: ConversationCreate, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """Create a new conversation.""" + conversation_service = ConversationService(db) + conversation = conversation_service.create_conversation( + user_id=current_user.id, + conversation_data=conversation_data + ) + return ConversationResponse.from_orm(conversation) + + +@router.get("/conversations", response_model=List[ConversationResponse]) +async def list_conversations( + skip: int = 0, + limit: int = 50, + search: str = None, + include_archived: bool = False, + order_by: str = "updated_at", + order_desc: bool = True, + db: Session = Depends(get_db) +): + """List user's conversations with search and filtering.""" + conversation_service = ConversationService(db) + conversations = conversation_service.get_user_conversations( + skip=skip, + limit=limit, + search_query=search, + include_archived=include_archived, + order_by=order_by, + order_desc=order_desc + ) + return [ConversationResponse.from_orm(conv) for conv in conversations] + + +@router.get("/conversations/count") +async def get_conversations_count( + search: str = None, + include_archived: bool = False, + db: Session = Depends(get_db) +): + """Get total count of conversations.""" + conversation_service = ConversationService(db) + count = conversation_service.get_user_conversations_count( + search_query=search, + include_archived=include_archived + ) + return {"count": count} + + +@router.get("/conversations/{conversation_id}", response_model=ConversationResponse) +async def get_conversation( + conversation_id: int, + db: Session = Depends(get_db) +): + """Get a specific conversation.""" + conversation_service = ConversationService(db) + conversation = conversation_service.get_conversation( + conversation_id=conversation_id + ) + if not conversation: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Conversation not found" + ) + return ConversationResponse.from_orm(conversation) + + +@router.put("/conversations/{conversation_id}", response_model=ConversationResponse) +async def update_conversation( + conversation_id: int, + conversation_update: ConversationUpdate, + db: Session = Depends(get_db) +): + """Update a conversation.""" + conversation_service = ConversationService(db) + updated_conversation = conversation_service.update_conversation( + conversation_id, conversation_update + ) + return ConversationResponse.from_orm(updated_conversation) + + +@router.delete("/conversations/{conversation_id}") +async def delete_conversation( + conversation_id: int, + db: Session = Depends(get_db) +): + """Delete a conversation.""" + conversation_service = ConversationService(db) + conversation_service.delete_conversation(conversation_id) + return {"message": "Conversation deleted successfully"} + + +@router.delete("/conversations") +async def delete_all_conversations( + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """Delete all conversations.""" + conversation_service = ConversationService(db) + conversation_service.delete_all_conversations() + return {"message": "All conversations deleted successfully"} + + +@router.put("/conversations/{conversation_id}/archive") +async def archive_conversation( + conversation_id: int, + db: Session = Depends(get_db) +): + """Archive a conversation.""" + conversation_service = ConversationService(db) + success = conversation_service.archive_conversation(conversation_id) + if not success: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Failed to archive conversation" + ) + + return {"message": "Conversation archived successfully"} + + +@router.put("/conversations/{conversation_id}/unarchive") +async def unarchive_conversation( + conversation_id: int, + db: Session = Depends(get_db) +): + """Unarchive a conversation.""" + conversation_service = ConversationService(db) + success = conversation_service.unarchive_conversation(conversation_id) + if not success: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Failed to unarchive conversation" + ) + + return {"message": "Conversation unarchived successfully"} + + +# Message management +@router.get("/conversations/{conversation_id}/messages", response_model=List[MessageResponse]) +async def get_conversation_messages( + conversation_id: int, + skip: int = 0, + limit: int = 100, + db: Session = Depends(get_db) +): + """Get messages from a conversation.""" + conversation_service = ConversationService(db) + messages = conversation_service.get_conversation_messages( + conversation_id, skip=skip, limit=limit + ) + return [MessageResponse.from_orm(msg) for msg in messages] + + +# Chat functionality +@router.post("/conversations/{conversation_id}/chat", response_model=ChatResponse) +async def chat( + conversation_id: int, + chat_request: ChatRequest, + db: Session = Depends(get_db) +): + """Send a message and get AI response.""" + chat_service = ChatService(db) + response = await chat_service.chat( + conversation_id=conversation_id, + message=chat_request.message, + stream=False, + temperature=chat_request.temperature, + max_tokens=chat_request.max_tokens, + use_agent=chat_request.use_agent, + use_langgraph=chat_request.use_langgraph, + use_knowledge_base=chat_request.use_knowledge_base, + knowledge_base_id=chat_request.knowledge_base_id + ) + + return response + + +@router.post("/conversations/{conversation_id}/chat/stream") +async def chat_stream( + conversation_id: int, + chat_request: ChatRequest, + db: Session = Depends(get_db) +): + """Send a message and get streaming AI response.""" + chat_service = ChatService(db) + + async def generate_response(): + async for chunk in chat_service.chat_stream( + conversation_id=conversation_id, + message=chat_request.message, + temperature=chat_request.temperature, + max_tokens=chat_request.max_tokens, + use_agent=chat_request.use_agent, + use_langgraph=chat_request.use_langgraph, + use_knowledge_base=chat_request.use_knowledge_base, + knowledge_base_id=chat_request.knowledge_base_id + ): + yield f"data: {chunk}\n\n" + + return StreamingResponse( + generate_response(), + media_type="text/plain", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + } + ) diff --git a/backend/th_agenter/api/endpoints/database_config.py b/backend/th_agenter/api/endpoints/database_config.py new file mode 100644 index 0000000..f526921 --- /dev/null +++ b/backend/th_agenter/api/endpoints/database_config.py @@ -0,0 +1,207 @@ +"""数据库配置管理API""" + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List, Dict, Any +from pydantic import BaseModel, Field +from th_agenter.models.user import User +from th_agenter.db.database import get_db +from th_agenter.services.database_config_service import DatabaseConfigService +from th_agenter.utils.logger import get_logger +from th_agenter.services.auth import AuthService +logger = get_logger("database_config_api") +router = APIRouter(prefix="/api/database-config", tags=["database-config"]) +from th_agenter.utils.schemas import FileListResponse,ExcelPreviewRequest,NormalResponse + +# 在文件顶部添加 +from functools import lru_cache + +# 创建服务单例 +@lru_cache() +def get_database_config_service() -> DatabaseConfigService: + """获取DatabaseConfigService单例""" + # 注意:这里需要处理db session的问题 + return DatabaseConfigService(None) # 临时方案 + +# 或者使用全局变量 +_database_service_instance = None + +def get_database_service(db: Session = Depends(get_db)) -> DatabaseConfigService: + """获取DatabaseConfigService实例""" + global _database_service_instance + if _database_service_instance is None: + _database_service_instance = DatabaseConfigService(db) + else: + # 更新db session + _database_service_instance.db = db + return _database_service_instance +class DatabaseConfigCreate(BaseModel): + name: str = Field(..., description="配置名称") + db_type: str = Field(default="postgresql", description="数据库类型") + host: str = Field(..., description="主机地址") + port: int = Field(..., description="端口号") + database: str = Field(..., description="数据库名") + username: str = Field(..., description="用户名") + password: str = Field(..., description="密码") + is_default: bool = Field(default=False, description="是否为默认配置") + connection_params: Dict[str, Any] = Field(default=None, description="额外连接参数") + + +class DatabaseConfigResponse(BaseModel): + id: int + name: str + db_type: str + host: str + port: int + database: str + username: str + password: str # 添加密码字段 + is_active: bool + is_default: bool + created_at: str + updated_at: str = None + + +@router.post("/", response_model=NormalResponse) +async def create_database_config( + config_data: DatabaseConfigCreate, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """创建或更新数据库配置""" + try: + service = DatabaseConfigService(db) + config = await service.create_or_update_config(current_user.id, config_data.dict()) + return NormalResponse( + success=True, + message="保存数据库配置成功", + data=config + ) + except Exception as e: + logger.error(f"创建或更新数据库配置失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + + +@router.get("/", response_model=List[DatabaseConfigResponse]) +async def get_database_configs( + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """获取用户的数据库配置列表""" + try: + service = DatabaseConfigService(db) + configs = service.get_user_configs(current_user.id) + + config_list = [config.to_dict(include_password=True, decrypt_service=service) for config in configs] + return config_list + except Exception as e: + logger.error(f"获取数据库配置失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e) + ) + + +@router.post("/{config_id}/test") +async def test_database_connection( + config_id: int, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """测试数据库连接""" + try: + service = DatabaseConfigService(db) + result = await service.test_connection(config_id, current_user.id) + return result + except Exception as e: + logger.error(f"测试数据库连接失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + + +@router.post("/{config_id}/connect") +async def connect_database( + config_id: int, + current_user: User = Depends(AuthService.get_current_user), + service: DatabaseConfigService = Depends(get_database_service) +): + """连接数据库并获取表列表""" + try: + result = await service.connect_and_get_tables(config_id, current_user.id) + return result + except Exception as e: + logger.error(f"连接数据库失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + + +@router.get("/tables/{table_name}/data") +async def get_table_data( + table_name: str, + db_type: str, + limit: int = 100, + current_user: User = Depends(AuthService.get_current_user), + service: DatabaseConfigService = Depends(get_database_service) +): + """获取表数据预览""" + try: + result = await service.get_table_data(table_name, current_user.id, db_type, limit) + return result + except Exception as e: + logger.error(f"获取表数据失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + + +@router.get("/tables/{table_name}/schema") +async def get_table_schema( + table_name: str, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """获取表结构信息""" + try: + service = DatabaseConfigService(db) + result = await service.describe_table(table_name, current_user.id) + return result + except Exception as e: + logger.error(f"获取表结构失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + + +@router.get("/by-type/{db_type}", response_model=DatabaseConfigResponse) +async def get_config_by_type( + db_type: str, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """根据数据库类型获取配置""" + try: + service = DatabaseConfigService(db) + config = service.get_config_by_type(current_user.id, db_type) + if not config: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"未找到类型为 {db_type} 的配置" + ) + # 返回包含解密密码的配置 + return config.to_dict(include_password=True, decrypt_service=service) + except Exception as e: + logger.error(f"获取数据库配置失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=str(e) + ) + diff --git a/backend/th_agenter/api/endpoints/knowledge_base.py b/backend/th_agenter/api/endpoints/knowledge_base.py new file mode 100644 index 0000000..4dee618 --- /dev/null +++ b/backend/th_agenter/api/endpoints/knowledge_base.py @@ -0,0 +1,666 @@ +"""Knowledge base API endpoints.""" + +import os +import logging +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form, status +from fastapi.responses import JSONResponse +from sqlalchemy.orm import Session + +from ...db.database import get_db +from ...models.user import User +from ...models.knowledge_base import KnowledgeBase, Document +from ...services.knowledge_base import KnowledgeBaseService +from ...services.document import DocumentService +from ...services.auth import AuthService +from ...utils.schemas import ( + KnowledgeBaseCreate, + KnowledgeBaseResponse, + DocumentResponse, + DocumentListResponse, + DocumentUpload, + DocumentProcessingStatus, + DocumentChunksResponse, + ErrorResponse +) +from ...utils.file_utils import FileUtils +from ...core.config import settings + +logger = logging.getLogger(__name__) +router = APIRouter(tags=["knowledge-bases"]) + + +@router.post("/", response_model=KnowledgeBaseResponse) +async def create_knowledge_base( + kb_data: KnowledgeBaseCreate, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Create a new knowledge base.""" + try: + # Check if knowledge base with same name already exists for this user + service = KnowledgeBaseService(db) + existing_kb = service.get_knowledge_base_by_name(kb_data.name) + if existing_kb: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Knowledge base with this name already exists" + ) + + # Create knowledge base + kb = service.create_knowledge_base(kb_data) + + return KnowledgeBaseResponse( + id=kb.id, + created_at=kb.created_at, + updated_at=kb.updated_at, + name=kb.name, + description=kb.description, + embedding_model=kb.embedding_model, + chunk_size=kb.chunk_size, + chunk_overlap=kb.chunk_overlap, + is_active=kb.is_active, + vector_db_type=kb.vector_db_type, + collection_name=kb.collection_name, + document_count=0, + active_document_count=0 + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to create knowledge base: {str(e)}" + ) + + +@router.get("/", response_model=List[KnowledgeBaseResponse]) +async def list_knowledge_bases( + skip: int = 0, + limit: int = 100, + search: Optional[str] = None, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """List knowledge bases for current user.""" + try: + service = KnowledgeBaseService(db) + knowledge_bases = service.get_knowledge_bases(skip=skip, limit=limit) + + result = [] + for kb in knowledge_bases: + # Count documents + total_docs = db.query(Document).filter( + Document.knowledge_base_id == kb.id + ).count() + + active_docs = db.query(Document).filter( + Document.knowledge_base_id == kb.id, + Document.is_processed == True + ).count() + + result.append(KnowledgeBaseResponse( + id=kb.id, + created_at=kb.created_at, + updated_at=kb.updated_at, + name=kb.name, + description=kb.description, + embedding_model=kb.embedding_model, + chunk_size=kb.chunk_size, + chunk_overlap=kb.chunk_overlap, + is_active=kb.is_active, + vector_db_type=kb.vector_db_type, + collection_name=kb.collection_name, + document_count=total_docs, + active_document_count=active_docs + )) + + return result + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to list knowledge bases: {str(e)}" + ) + + +@router.get("/{kb_id}", response_model=KnowledgeBaseResponse) +async def get_knowledge_base( + kb_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Get knowledge base by ID.""" + try: + service = KnowledgeBaseService(db) + kb = service.get_knowledge_base(kb_id) + if not kb: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + # Count documents + total_docs = db.query(Document).filter( + Document.knowledge_base_id == kb.id + ).count() + + active_docs = db.query(Document).filter( + Document.knowledge_base_id == kb.id, + Document.is_processed == True + ).count() + + return KnowledgeBaseResponse( + id=kb.id, + created_at=kb.created_at, + updated_at=kb.updated_at, + name=kb.name, + description=kb.description, + embedding_model=kb.embedding_model, + chunk_size=kb.chunk_size, + chunk_overlap=kb.chunk_overlap, + is_active=kb.is_active, + vector_db_type=kb.vector_db_type, + collection_name=kb.collection_name, + document_count=total_docs, + active_document_count=active_docs + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to get knowledge base: {str(e)}" + ) + + +@router.put("/{kb_id}", response_model=KnowledgeBaseResponse) +async def update_knowledge_base( + kb_id: int, + kb_data: KnowledgeBaseCreate, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Update knowledge base.""" + try: + service = KnowledgeBaseService(db) + kb = service.update_knowledge_base(kb_id, kb_data) + if not kb: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + # Count documents + total_docs = db.query(Document).filter( + Document.knowledge_base_id == kb.id + ).count() + + active_docs = db.query(Document).filter( + Document.knowledge_base_id == kb.id, + Document.is_processed == True + ).count() + + return KnowledgeBaseResponse( + id=kb.id, + created_at=kb.created_at, + updated_at=kb.updated_at, + name=kb.name, + description=kb.description, + embedding_model=kb.embedding_model, + chunk_size=kb.chunk_size, + chunk_overlap=kb.chunk_overlap, + is_active=kb.is_active, + vector_db_type=kb.vector_db_type, + collection_name=kb.collection_name, + document_count=total_docs, + active_document_count=active_docs + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to update knowledge base: {str(e)}" + ) + + +@router.delete("/{kb_id}") +async def delete_knowledge_base( + kb_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Delete knowledge base.""" + try: + service = KnowledgeBaseService(db) + success = service.delete_knowledge_base(kb_id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + return {"message": "Knowledge base deleted successfully"} + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to delete knowledge base: {str(e)}" + ) + + +# Document management endpoints +@router.post("/{kb_id}/documents", response_model=DocumentResponse) +async def upload_document( + kb_id: int, + file: UploadFile = File(...), + process_immediately: bool = Form(True), + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Upload document to knowledge base.""" + try: + # Verify knowledge base exists and user has access + kb_service = KnowledgeBaseService(db) + kb = kb_service.get_knowledge_base(kb_id) + if not kb: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + # Validate file + if not FileUtils.validate_file_extension(file.filename): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"File type not supported. Allowed types: {', '.join(FileUtils.ALLOWED_EXTENSIONS)}" + ) + + # Check file size (50MB limit) + max_size = 50 * 1024 * 1024 # 50MB + if file.size and file.size > max_size: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"File too large. Maximum size: {FileUtils.format_file_size(max_size)}" + ) + + # Upload document + doc_service = DocumentService(db) + document = await doc_service.upload_document( + file, kb_id + ) + + # Process document immediately if requested + if process_immediately: + try: + await doc_service.process_document(document.id, kb_id) + # Refresh document to get updated status + db.refresh(document) + except Exception as e: + # Log error but don't fail the upload + logger.error(f"Failed to process document immediately: {e}") + + return DocumentResponse( + id=document.id, + created_at=document.created_at, + updated_at=document.updated_at, + knowledge_base_id=document.knowledge_base_id, + filename=document.filename, + original_filename=document.original_filename, + file_path=document.file_path, + file_type=document.file_type, + file_size=document.file_size, + mime_type=document.mime_type, + is_processed=document.is_processed, + processing_error=document.processing_error, + chunk_count=document.chunk_count or 0, + embedding_model=document.embedding_model, + file_size_mb=round(document.file_size / (1024 * 1024), 2) + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to upload document: {str(e)}" + ) + + +@router.get("/{kb_id}/documents", response_model=DocumentListResponse) +async def list_documents( + kb_id: int, + skip: int = 0, + limit: int = 50, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """List documents in knowledge base.""" + try: + # Verify knowledge base exists and user has access + kb_service = KnowledgeBaseService(db) + kb = kb_service.get_knowledge_base(kb_id) + if not kb: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + doc_service = DocumentService(db) + documents, total = doc_service.list_documents(kb_id, skip, limit) + + doc_responses = [] + for doc in documents: + doc_responses.append(DocumentResponse( + id=doc.id, + created_at=doc.created_at, + updated_at=doc.updated_at, + knowledge_base_id=doc.knowledge_base_id, + filename=doc.filename, + original_filename=doc.original_filename, + file_path=doc.file_path, + file_type=doc.file_type, + file_size=doc.file_size, + mime_type=doc.mime_type, + is_processed=doc.is_processed, + processing_error=doc.processing_error, + chunk_count=doc.chunk_count or 0, + embedding_model=doc.embedding_model, + file_size_mb=round(doc.file_size / (1024 * 1024), 2) + )) + + return DocumentListResponse( + documents=doc_responses, + total=total, + page=skip // limit + 1, + page_size=limit + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to list documents: {str(e)}" + ) + + +@router.get("/{kb_id}/documents/{doc_id}", response_model=DocumentResponse) +async def get_document( + kb_id: int, + doc_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Get document by ID.""" + try: + # Verify knowledge base exists and user has access + kb_service = KnowledgeBaseService(db) + kb = kb_service.get_knowledge_base(kb_id) + if not kb: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + doc_service = DocumentService(db) + document = doc_service.get_document(doc_id, kb_id) + if not document: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Document not found" + ) + + return DocumentResponse( + id=document.id, + created_at=document.created_at, + updated_at=document.updated_at, + knowledge_base_id=document.knowledge_base_id, + filename=document.filename, + original_filename=document.original_filename, + file_path=document.file_path, + file_type=document.file_type, + file_size=document.file_size, + mime_type=document.mime_type, + is_processed=document.is_processed, + processing_error=document.processing_error, + chunk_count=document.chunk_count or 0, + embedding_model=document.embedding_model, + file_size_mb=round(document.file_size / (1024 * 1024), 2) + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to get document: {str(e)}" + ) + + +@router.delete("/{kb_id}/documents/{doc_id}") +async def delete_document( + kb_id: int, + doc_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Delete document from knowledge base.""" + try: + # Verify knowledge base exists and user has access + kb_service = KnowledgeBaseService(db) + kb = kb_service.get_knowledge_base(kb_id) + if not kb: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + doc_service = DocumentService(db) + success = doc_service.delete_document(doc_id, kb_id) + if not success: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Document not found" + ) + + return {"message": "Document deleted successfully"} + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to delete document: {str(e)}" + ) + + +@router.post("/{kb_id}/documents/{doc_id}/process", response_model=DocumentProcessingStatus) +async def process_document( + kb_id: int, + doc_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Process document for vector search.""" + try: + # Verify knowledge base exists and user has access + kb_service = KnowledgeBaseService(db) + kb = kb_service.get_knowledge_base(kb_id) + if not kb: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + # Check if document exists + doc_service = DocumentService(db) + document = doc_service.get_document(doc_id, kb_id) + if not document: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Document not found" + ) + + # Process the document + result = await doc_service.process_document(doc_id, kb_id) + + return DocumentProcessingStatus( + document_id=doc_id, + status=result["status"], + progress=result.get("progress", 0.0), + error_message=result.get("error_message"), + chunks_created=result.get("chunks_created", 0) + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to process document: {str(e)}" + ) + + +@router.get("/{kb_id}/documents/{doc_id}/status", response_model=DocumentProcessingStatus) +async def get_document_processing_status( + kb_id: int, + doc_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Get document processing status.""" + try: + # Verify knowledge base exists and user has access + kb_service = KnowledgeBaseService(db) + kb = kb_service.get_knowledge_base(kb_id) + if not kb: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + doc_service = DocumentService(db) + document = doc_service.get_document(doc_id, kb_id) + if not document: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Document not found" + ) + + # Determine status + if document.processing_error: + status_str = "failed" + progress = 0.0 + elif document.is_processed: + status_str = "completed" + progress = 100.0 + else: + status_str = "pending" + progress = 0.0 + + return DocumentProcessingStatus( + document_id=document.id, + status=status_str, + progress=progress, + error_message=document.processing_error, + chunks_created=document.chunk_count or 0 + ) + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to get document status: {str(e)}" + ) + + +@router.get("/{kb_id}/search") +async def search_knowledge_base( + kb_id: int, + query: str, + limit: int = 5, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """Search documents in a knowledge base.""" + try: + # Verify knowledge base exists and user has access + kb_service = KnowledgeBaseService(db) + kb = kb_service.get_knowledge_base(kb_id) + if not kb: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + # Perform search + doc_service = DocumentService(db) + results = doc_service.search_documents(kb_id, query, limit) + + return { + "knowledge_base_id": kb_id, + "query": query, + "results": results, + "total_results": len(results) + } + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to search knowledge base: {str(e)}" + ) + + +@router.get("/{kb_id}/documents/{doc_id}/chunks", response_model=DocumentChunksResponse) +async def get_document_chunks( + kb_id: int, + doc_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """ + Get document chunks (segments) for a specific document. + + Args: + kb_id: Knowledge base ID + doc_id: Document ID + db: Database session + current_user: Current authenticated user + + Returns: + DocumentChunksResponse: Document chunks with metadata + """ + try: + # Verify knowledge base exists and user has access + kb_service = KnowledgeBaseService(db) + knowledge_base = kb_service.get_knowledge_base(kb_id) + if not knowledge_base: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Knowledge base not found" + ) + + # Verify document exists in the knowledge base + doc_service = DocumentService(db) + document = doc_service.get_document(doc_id, kb_id) + if not document: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="Document not found" + ) + + # Get document chunks + chunks = doc_service.get_document_chunks(doc_id) + + return DocumentChunksResponse( + document_id=doc_id, + document_name=document.filename, + total_chunks=len(chunks), + chunks=chunks + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"Failed to get document chunks: {str(e)}" + ) \ No newline at end of file diff --git a/backend/th_agenter/api/endpoints/llm_configs.py b/backend/th_agenter/api/endpoints/llm_configs.py new file mode 100644 index 0000000..8532fd4 --- /dev/null +++ b/backend/th_agenter/api/endpoints/llm_configs.py @@ -0,0 +1,528 @@ +"""LLM configuration management API endpoints.""" + +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, status, Query +from sqlalchemy.orm import Session +from sqlalchemy import or_ + +from ...db.database import get_db +from ...models.user import User +from ...models.llm_config import LLMConfig +from ...core.simple_permissions import require_super_admin, require_authenticated_user +from ...services.auth import AuthService +from ...utils.logger import get_logger +from ...schemas.llm_config import ( + LLMConfigCreate, LLMConfigUpdate, LLMConfigResponse, + LLMConfigTest +) +from th_agenter.services.document_processor import get_document_processor +logger = get_logger(__name__) +router = APIRouter(prefix="/llm-configs", tags=["llm-configs"]) + + +@router.get("/", response_model=List[LLMConfigResponse]) +async def get_llm_configs( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=1000), + search: Optional[str] = Query(None), + provider: Optional[str] = Query(None), + is_active: Optional[bool] = Query(None), + is_embedding: Optional[bool] = Query(None), + db: Session = Depends(get_db), + current_user: User = Depends(require_authenticated_user) +): + """获取大模型配置列表.""" + try: + query = db.query(LLMConfig) + + # 搜索 + if search: + query = query.filter( + or_( + LLMConfig.name.ilike(f"%{search}%"), + LLMConfig.model_name.ilike(f"%{search}%"), + LLMConfig.description.ilike(f"%{search}%") + ) + ) + + # 服务商筛选 + if provider: + query = query.filter(LLMConfig.provider == provider) + + # 状态筛选 + if is_active is not None: + query = query.filter(LLMConfig.is_active == is_active) + + # 模型类型筛选 + if is_embedding is not None: + query = query.filter(LLMConfig.is_embedding == is_embedding) + + # 排序 + query = query.order_by(LLMConfig.name) + + # 分页 + configs = query.offset(skip).limit(limit).all() + + return [config.to_dict(include_sensitive=True) for config in configs] + + except Exception as e: + logger.error(f"Error getting LLM configs: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取大模型配置列表失败" + ) + + +@router.get("/providers") +async def get_llm_providers( + db: Session = Depends(get_db), + current_user: User = Depends(require_authenticated_user) +): + """获取支持的大模型服务商列表.""" + try: + providers = db.query(LLMConfig.provider).distinct().all() + return [provider[0] for provider in providers if provider[0]] + + except Exception as e: + logger.error(f"Error getting LLM providers: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取服务商列表失败" + ) + + +@router.get("/active", response_model=List[LLMConfigResponse]) +async def get_active_llm_configs( + is_embedding: Optional[bool] = Query(None), + db: Session = Depends(get_db), + current_user: User = Depends(require_authenticated_user) +): + """获取所有激活的大模型配置.""" + try: + query = db.query(LLMConfig).filter(LLMConfig.is_active == True) + + if is_embedding is not None: + query = query.filter(LLMConfig.is_embedding == is_embedding) + + configs = query.order_by(LLMConfig.created_at).all() + + return [config.to_dict(include_sensitive=True) for config in configs] + + except Exception as e: + logger.error(f"Error getting active LLM configs: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取激活配置列表失败" + ) + + +@router.get("/default", response_model=LLMConfigResponse) +async def get_default_llm_config( + is_embedding: bool = Query(False, description="是否获取嵌入模型默认配置"), + db: Session = Depends(get_db), + current_user: User = Depends(require_authenticated_user) +): + """获取默认大模型配置.""" + try: + config = db.query(LLMConfig).filter( + LLMConfig.is_default == True, + LLMConfig.is_embedding == is_embedding, + LLMConfig.is_active == True + ).first() + + if not config: + model_type = "嵌入模型" if is_embedding else "对话模型" + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f"未找到默认{model_type}配置" + ) + + return config.to_dict(include_sensitive=True) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error getting default LLM config: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取默认配置失败" + ) + + +@router.get("/{config_id}", response_model=LLMConfigResponse) +async def get_llm_config( + config_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_authenticated_user) +): + """获取大模型配置详情.""" + try: + config = db.query(LLMConfig).filter(LLMConfig.id == config_id).first() + if not config: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="大模型配置不存在" + ) + + return config.to_dict(include_sensitive=True) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error getting LLM config {config_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取大模型配置详情失败" + ) + + +@router.post("/", response_model=LLMConfigResponse, status_code=status.HTTP_201_CREATED) +async def create_llm_config( + config_data: LLMConfigCreate, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """创建大模型配置.""" + try: + # 检查配置名称是否已存在 + existing_config = db.query(LLMConfig).filter( + LLMConfig.name == config_data.name + ).first() + if existing_config: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="配置名称已存在" + ) + + # 创建临时配置对象进行验证 + temp_config = LLMConfig( + name=config_data.name, + provider=config_data.provider, + model_name=config_data.model_name, + api_key=config_data.api_key, + base_url=config_data.base_url, + max_tokens=config_data.max_tokens, + temperature=config_data.temperature, + top_p=config_data.top_p, + frequency_penalty=config_data.frequency_penalty, + presence_penalty=config_data.presence_penalty, + description=config_data.description, + is_active=config_data.is_active, + is_default=config_data.is_default, + is_embedding=config_data.is_embedding, + extra_config=config_data.extra_config or {} + ) + + # 验证配置 + validation_result = temp_config.validate_config() + if not validation_result['valid']: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=validation_result['error'] + ) + + # 如果设为默认,取消同类型的其他默认配置 + if config_data.is_default: + db.query(LLMConfig).filter( + LLMConfig.is_embedding == config_data.is_embedding + ).update({"is_default": False}) + + # 创建配置 + config = LLMConfig( + name=config_data.name, + provider=config_data.provider, + model_name=config_data.model_name, + api_key=config_data.api_key, + base_url=config_data.base_url, + max_tokens=config_data.max_tokens, + temperature=config_data.temperature, + top_p=config_data.top_p, + frequency_penalty=config_data.frequency_penalty, + presence_penalty=config_data.presence_penalty, + description=config_data.description, + is_active=config_data.is_active, + is_default=config_data.is_default, + is_embedding=config_data.is_embedding, + extra_config=config_data.extra_config or {} + ) + config.set_audit_fields(current_user.id) + + db.add(config) + db.commit() + db.refresh(config) + + logger.info(f"LLM config created: {config.name} by user {current_user.username}") + return config.to_dict() + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error creating LLM config: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="创建大模型配置失败" + ) + + +@router.put("/{config_id}", response_model=LLMConfigResponse) +async def update_llm_config( + config_id: int, + config_data: LLMConfigUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """更新大模型配置.""" + try: + config = db.query(LLMConfig).filter(LLMConfig.id == config_id).first() + if not config: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="大模型配置不存在" + ) + + # 检查配置名称是否已存在(排除自己) + if config_data.name and config_data.name != config.name: + existing_config = db.query(LLMConfig).filter( + LLMConfig.name == config_data.name, + LLMConfig.id != config_id + ).first() + if existing_config: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="配置名称已存在" + ) + + # 如果设为默认,取消同类型的其他默认配置 + if config_data.is_default is True: + # 获取当前配置的embedding类型,如果更新中包含is_embedding则使用新值 + is_embedding = config_data.is_embedding if config_data.is_embedding is not None else config.is_embedding + db.query(LLMConfig).filter( + LLMConfig.is_embedding == is_embedding, + LLMConfig.id != config_id + ).update({"is_default": False}) + + # 更新字段 + update_data = config_data.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(config, field, value) + + config.set_audit_fields(current_user.id, is_update=True) + + db.commit() + db.refresh(config) + + logger.info(f"LLM config updated: {config.name} by user {current_user.username}") + return config.to_dict() + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error updating LLM config {config_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="更新大模型配置失败" + ) + + +@router.delete("/{config_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_llm_config( + config_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """删除大模型配置.""" + try: + config = db.query(LLMConfig).filter(LLMConfig.id == config_id).first() + if not config: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="大模型配置不存在" + ) + + # TODO: 检查是否有对话或其他功能正在使用该配置 + # 这里可以添加相关的检查逻辑 + + db.delete(config) + db.commit() + + logger.info(f"LLM config deleted: {config.name} by user {current_user.username}") + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error deleting LLM config {config_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="删除大模型配置失败" + ) + + +@router.post("/{config_id}/test") +async def test_llm_config( + config_id: int, + test_data: LLMConfigTest, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """测试大模型配置.""" + try: + config = db.query(LLMConfig).filter(LLMConfig.id == config_id).first() + if not config: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="大模型配置不存在" + ) + + # 验证配置 + validation_result = config.validate_config() + if not validation_result["valid"]: + return { + "success": False, + "message": f"配置验证失败: {validation_result['error']}", + "details": validation_result + } + + # 尝试创建客户端并发送测试请求 + try: + # 这里应该根据不同的服务商创建相应的客户端 + # 由于具体的客户端实现可能因服务商而异,这里提供一个通用的框架 + + test_message = test_data.message or "Hello, this is a test message." + + # TODO: 实现具体的测试逻辑 + # 例如: + # client = config.get_client() + # response = client.chat.completions.create( + # model=config.model_name, + # messages=[{"role": "user", "content": test_message}], + # max_tokens=100 + # ) + + # 模拟测试成功 + logger.info(f"LLM config test: {config.name} by user {current_user.username}") + + return { + "success": True, + "message": "配置测试成功", + "test_message": test_message, + "response": "这是一个模拟的测试响应。实际实现中,这里会是大模型的真实响应。", + "latency_ms": 150, # 模拟延迟 + "config_info": config.get_client_config() + } + + except Exception as test_error: + logger.error(f"LLM config test failed: {config.name}, error: {str(test_error)}") + return { + "success": False, + "message": f"配置测试失败: {str(test_error)}", + "test_message": test_message, + "config_info": config.get_client_config() + } + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error testing LLM config {config_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="测试大模型配置失败" + ) + + +@router.post("/{config_id}/toggle-status") +async def toggle_llm_config_status( + config_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """切换大模型配置状态.""" + try: + config = db.query(LLMConfig).filter(LLMConfig.id == config_id).first() + if not config: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="大模型配置不存在" + ) + + # 切换状态 + config.is_active = not config.is_active + config.set_audit_fields(current_user.id, is_update=True) + + db.commit() + db.refresh(config) + + status_text = "激活" if config.is_active else "禁用" + logger.info(f"LLM config status toggled: {config.name} {status_text} by user {current_user.username}") + + return { + "message": f"配置已{status_text}", + "is_active": config.is_active + } + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error toggling LLM config status {config_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="切换配置状态失败" + ) + + +@router.post("/{config_id}/set-default") +async def set_default_llm_config( + config_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """设置默认大模型配置.""" + try: + config = db.query(LLMConfig).filter(LLMConfig.id == config_id).first() + if not config: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="大模型配置不存在" + ) + + # 检查配置是否激活 + if not config.is_active: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="只能将激活的配置设为默认" + ) + + # 取消同类型的其他默认配置 + db.query(LLMConfig).filter( + LLMConfig.is_embedding == config.is_embedding, + LLMConfig.id != config_id + ).update({"is_default": False}) + + # 设置当前配置为默认 + config.is_default = True + config.set_audit_fields(current_user.id, is_update=True) + + db.commit() + db.refresh(config) + + model_type = "嵌入模型" if config.is_embedding else "对话模型" + logger.info(f"Default LLM config set: {config.name} ({model_type}) by user {current_user.username}") + # 更新文档处理器默认embedding + get_document_processor()._init_embeddings() + return { + "message": f"已将 {config.name} 设为默认{model_type}配置", + "is_default": config.is_default + } + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error setting default LLM config {config_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="设置默认配置失败" + ) \ No newline at end of file diff --git a/backend/th_agenter/api/endpoints/roles.py b/backend/th_agenter/api/endpoints/roles.py new file mode 100644 index 0000000..0b88249 --- /dev/null +++ b/backend/th_agenter/api/endpoints/roles.py @@ -0,0 +1,346 @@ +"""Role management API endpoints.""" + +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, status, Query +from sqlalchemy.orm import Session +from sqlalchemy import and_, or_ + +from ...core.simple_permissions import require_super_admin +from ...db.database import get_db +from ...models.user import User +from ...models.permission import Role, UserRole +from ...services.auth import AuthService +from ...utils.logger import get_logger +from ...schemas.permission import ( + RoleCreate, RoleUpdate, RoleResponse, + UserRoleAssign +) + +logger = get_logger(__name__) +router = APIRouter(prefix="/roles", tags=["roles"]) + + +@router.get("/", response_model=List[RoleResponse]) +async def get_roles( + skip: int = Query(0, ge=0), + limit: int = Query(100, ge=1, le=1000), + search: Optional[str] = Query(None), + is_active: Optional[bool] = Query(None), + db: Session = Depends(get_db), + current_user = Depends(require_super_admin), +): + """获取角色列表.""" + try: + query = db.query(Role) + + # 搜索 + if search: + query = query.filter( + or_( + Role.name.ilike(f"%{search}%"), + Role.code.ilike(f"%{search}%"), + Role.description.ilike(f"%{search}%") + ) + ) + + # 状态筛选 + if is_active is not None: + query = query.filter(Role.is_active == is_active) + + # 分页 + roles = query.offset(skip).limit(limit).all() + + return [role.to_dict() for role in roles] + + except Exception as e: + logger.error(f"Error getting roles: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取角色列表失败" + ) + + +@router.get("/{role_id}", response_model=RoleResponse) +async def get_role( + role_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """获取角色详情.""" + try: + role = db.query(Role).filter(Role.id == role_id).first() + if not role: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="角色不存在" + ) + + return role.to_dict() + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error getting role {role_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取角色详情失败" + ) + + +@router.post("/", response_model=RoleResponse, status_code=status.HTTP_201_CREATED) +async def create_role( + role_data: RoleCreate, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """创建角色.""" + try: + # 检查角色代码是否已存在 + existing_role = db.query(Role).filter( + Role.code == role_data.code + ).first() + if existing_role: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="角色代码已存在" + ) + + # 创建角色 + role = Role( + name=role_data.name, + code=role_data.code, + description=role_data.description, + is_active=role_data.is_active + ) + role.set_audit_fields(current_user.id) + + db.add(role) + db.commit() + db.refresh(role) + + logger.info(f"Role created: {role.name} by user {current_user.username}") + return role.to_dict() + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error creating role: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="创建角色失败" + ) + + +@router.put("/{role_id}", response_model=RoleResponse) +async def update_role( + role_id: int, + role_data: RoleUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """更新角色.""" + try: + role = db.query(Role).filter(Role.id == role_id).first() + if not role: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="角色不存在" + ) + + # 超级管理员角色不能被编辑 + if role.code == "SUPER_ADMIN": + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="超级管理员角色不能被编辑" + ) + + # 检查角色编码是否已存在(排除当前角色) + if role_data.code and role_data.code != role.code: + existing_role = db.query(Role).filter( + and_( + Role.code == role_data.code, + Role.id != role_id + ) + ).first() + if existing_role: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="角色代码已存在" + ) + + # 更新字段 + update_data = role_data.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(role, field, value) + + role.set_audit_fields(current_user.id, is_update=True) + + db.commit() + db.refresh(role) + + logger.info(f"Role updated: {role.name} by user {current_user.username}") + return role.to_dict() + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error updating role {role_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="更新角色失败" + ) + + +@router.delete("/{role_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_role( + role_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """删除角色.""" + try: + role = db.query(Role).filter(Role.id == role_id).first() + if not role: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="角色不存在" + ) + + # 超级管理员角色不能被删除 + if role.code == "SUPER_ADMIN": + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="超级管理员角色不能被删除" + ) + + # 检查是否有用户使用该角色 + user_count = db.query(UserRole).filter( + UserRole.role_id == role_id + ).count() + if user_count > 0: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"无法删除角色,还有 {user_count} 个用户关联此角色" + ) + + # 删除角色 + db.delete(role) + db.commit() + + logger.info(f"Role deleted: {role.name} by user {current_user.username}") + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error deleting role {role_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="删除角色失败" + ) + + +# 用户角色管理路由 +user_role_router = APIRouter(prefix="/user-roles", tags=["user-roles"]) + + +@user_role_router.post("/assign", status_code=status.HTTP_201_CREATED) +async def assign_user_roles( + assignment_data: UserRoleAssign, + db: Session = Depends(get_db), + current_user: User = Depends(require_super_admin) +): + """为用户分配角色.""" + try: + # 验证用户是否存在 + user = db.query(User).filter(User.id == assignment_data.user_id).first() + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="用户不存在" + ) + + # 验证角色是否存在 + roles = db.query(Role).filter( + Role.id.in_(assignment_data.role_ids) + ).all() + if len(roles) != len(assignment_data.role_ids): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="部分角色不存在" + ) + + # 删除现有角色关联 + db.query(UserRole).filter( + UserRole.user_id == assignment_data.user_id + ).delete() + + # 添加新的角色关联 + for role_id in assignment_data.role_ids: + user_role = UserRole( + user_id=assignment_data.user_id, + role_id=role_id + ) + db.add(user_role) + + db.commit() + + logger.info(f"User roles assigned: user {user.username}, roles {assignment_data.role_ids} by user {current_user.username}") + + return {"message": "角色分配成功"} + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error assigning roles to user {assignment_data.user_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="角色分配失败" + ) + + +@user_role_router.get("/user/{user_id}", response_model=List[RoleResponse]) +async def get_user_roles( + user_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_active_user) +): + """获取用户角色列表.""" + try: + # 检查权限:用户只能查看自己的角色,或者是超级管理员 + if current_user.id != user_id and not current_user.is_superuser(): + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="无权限查看其他用户的角色" + ) + + user = db.query(User).filter(User.id == user_id).first() + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="用户不存在" + ) + + roles = db.query(Role).join( + UserRole, Role.id == UserRole.role_id + ).filter( + UserRole.user_id == user_id + ).all() + + return [role.to_dict() for role in roles] + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error getting user roles {user_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取用户角色失败" + ) + + +# 将子路由添加到主路由 +router.include_router(user_role_router) \ No newline at end of file diff --git a/backend/th_agenter/api/endpoints/smart_chat.py b/backend/th_agenter/api/endpoints/smart_chat.py new file mode 100644 index 0000000..828d526 --- /dev/null +++ b/backend/th_agenter/api/endpoints/smart_chat.py @@ -0,0 +1,342 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.security import HTTPBearer +from sqlalchemy.orm import Session +from typing import Optional, Dict, Any +import logging +from datetime import datetime + +from th_agenter.db.database import get_db +from th_agenter.services.auth import AuthService +from th_agenter.services.smart_workflow import SmartWorkflowManager +from th_agenter.services.conversation import ConversationService +from th_agenter.services.conversation_context import conversation_context_service +from th_agenter.utils.schemas import BaseResponse +from pydantic import BaseModel + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/smart-chat", tags=["smart-chat"]) +security = HTTPBearer() + +# Request/Response Models +class SmartQueryRequest(BaseModel): + query: str + conversation_id: Optional[int] = None + is_new_conversation: bool = False + +class SmartQueryResponse(BaseModel): + success: bool + message: str + data: Optional[Dict[str, Any]] = None + workflow_steps: Optional[list] = None + conversation_id: Optional[int] = None + +class ConversationContextResponse(BaseModel): + success: bool + message: str + data: Optional[Dict[str, Any]] = None + +@router.post("/query", response_model=SmartQueryResponse) +async def smart_query( + request: SmartQueryRequest, + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 智能问数查询接口 + 支持新对话时自动加载文件列表,智能选择相关Excel文件,生成和执行pandas代码 + """ + conversation_id = None + + try: + # 验证请求参数 + if not request.query or not request.query.strip(): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="查询内容不能为空" + ) + + if len(request.query) > 1000: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="查询内容过长,请控制在1000字符以内" + ) + + # 初始化工作流管理器 + workflow_manager = SmartWorkflowManager(db) + conversation_service = ConversationService(db) + + # 处理对话上下文 + conversation_id = request.conversation_id + + # 如果是新对话或没有指定对话ID,创建新对话 + if request.is_new_conversation or not conversation_id: + try: + conversation_id = await conversation_context_service.create_conversation( + user_id=current_user.id, + title=f"智能问数: {request.query[:20]}..." + ) + request.is_new_conversation = True + logger.info(f"创建新对话: {conversation_id}") + except Exception as e: + logger.warning(f"创建对话失败,使用临时会话: {e}") + conversation_id = None + else: + # 验证对话是否存在且属于当前用户 + try: + context = await conversation_context_service.get_conversation_context(conversation_id) + if not context or context.get('user_id') != current_user.id: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="对话不存在或无权访问" + ) + logger.info(f"使用现有对话: {conversation_id}") + except HTTPException: + raise + except Exception as e: + logger.error(f"验证对话失败: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="对话验证失败" + ) + + # 保存用户消息 + if conversation_id: + try: + await conversation_context_service.save_message( + conversation_id=conversation_id, + role="user", + content=request.query + ) + except Exception as e: + logger.warning(f"保存用户消息失败: {e}") + # 不阻断流程,继续执行查询 + + # 执行智能查询工作流 + try: + result = await workflow_manager.process_smart_query( + user_query=request.query, + user_id=current_user.id, + conversation_id=conversation_id, + is_new_conversation=request.is_new_conversation + ) + except Exception as e: + logger.error(f"智能查询执行失败: {e}") + # 返回结构化的错误响应 + return SmartQueryResponse( + success=False, + message=f"查询执行失败: {str(e)}", + data={'error_type': 'query_execution_error'}, + workflow_steps=[{ + 'step': 'error', + 'status': 'failed', + 'message': str(e) + }], + conversation_id=conversation_id + ) + + # 如果查询成功,保存助手回复和更新上下文 + if result['success'] and conversation_id: + try: + # 保存助手回复 + await conversation_context_service.save_message( + conversation_id=conversation_id, + role="assistant", + content=result.get('data', {}).get('summary', '查询完成'), + metadata={ + 'query_result': result.get('data'), + 'workflow_steps': result.get('workflow_steps', []), + 'selected_files': result.get('data', {}).get('used_files', []) + } + ) + + # 更新对话上下文 + await conversation_context_service.update_conversation_context( + conversation_id=conversation_id, + query=request.query, + selected_files=result.get('data', {}).get('used_files', []) + ) + + logger.info(f"查询成功完成,对话ID: {conversation_id}") + + except Exception as e: + logger.warning(f"保存消息到对话历史失败: {e}") + # 不影响返回结果,只记录警告 + + # 返回结果,包含对话ID + response_data = result.get('data', {}) + if conversation_id: + response_data['conversation_id'] = conversation_id + + return SmartQueryResponse( + success=result['success'], + message=result.get('message', '查询完成'), + data=response_data, + workflow_steps=result.get('workflow_steps', []), + conversation_id=conversation_id + ) + + except HTTPException: + print(e) + raise + except Exception as e: + logger.error(f"智能查询接口异常: {e}", exc_info=True) + # 返回通用错误响应 + return SmartQueryResponse( + success=False, + message="服务器内部错误,请稍后重试", + data={'error_type': 'internal_server_error'}, + workflow_steps=[{ + 'step': 'error', + 'status': 'failed', + 'message': '系统异常' + }], + conversation_id=conversation_id + ) + +@router.get("/conversation/{conversation_id}/context", response_model=ConversationContextResponse) +async def get_conversation_context( + conversation_id: int, + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 获取对话上下文信息,包括已使用的文件和历史查询 + """ + try: + # 获取对话上下文 + context = await conversation_context_service.get_conversation_context(conversation_id) + + if not context: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="对话上下文不存在" + ) + + # 验证用户权限 + if context['user_id'] != current_user.id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="无权访问此对话" + ) + + # 获取对话历史 + history = await conversation_context_service.get_conversation_history(conversation_id) + context['message_history'] = history + + return ConversationContextResponse( + success=True, + message="获取对话上下文成功", + data=context + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"获取对话上下文失败: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"获取对话上下文失败: {str(e)}" + ) + +@router.get("/files/status", response_model=ConversationContextResponse) +async def get_files_status( + current_user = Depends(AuthService.get_current_user) +): + """ + 获取用户当前的文件状态和统计信息 + """ + try: + workflow_manager = SmartWorkflowManager() + + # 获取用户文件列表 + file_list = await workflow_manager.excel_workflow._load_user_file_list(current_user.id) + + # 统计信息 + total_files = len(file_list) + total_rows = sum(f.get('row_count', 0) for f in file_list) + total_columns = sum(f.get('column_count', 0) for f in file_list) + + # 文件类型统计 + file_types = {} + for file_info in file_list: + filename = file_info['filename'] + ext = filename.split('.')[-1].lower() if '.' in filename else 'unknown' + file_types[ext] = file_types.get(ext, 0) + 1 + + status_data = { + 'total_files': total_files, + 'total_rows': total_rows, + 'total_columns': total_columns, + 'file_types': file_types, + 'files': [{ + 'id': f['id'], + 'filename': f['filename'], + 'row_count': f.get('row_count', 0), + 'column_count': f.get('column_count', 0), + 'columns': f.get('columns', []), + 'upload_time': f.get('upload_time') + } for f in file_list], + 'ready_for_query': total_files > 0 + } + + return ConversationContextResponse( + success=True, + message=f"当前有{total_files}个可用文件" if total_files > 0 else "暂无可用文件,请先上传Excel文件", + data=status_data + ) + + except Exception as e: + logger.error(f"获取文件状态失败: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"获取文件状态失败: {str(e)}" + ) + +@router.post("/conversation/{conversation_id}/reset") +async def reset_conversation_context( + conversation_id: int, + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 重置对话上下文,清除历史查询记录但保留文件 + """ + try: + # 验证对话存在和用户权限 + context = await conversation_context_service.get_conversation_context(conversation_id) + + if not context: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="对话上下文不存在" + ) + + if context['user_id'] != current_user.id: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="无权访问此对话" + ) + + # 重置对话上下文 + success = await conversation_context_service.reset_conversation_context(conversation_id) + + if success: + return { + "success": True, + "message": "对话上下文已重置,可以开始新的数据分析会话" + } + else: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="重置对话上下文失败" + ) + + except HTTPException: + raise + except Exception as e: + logger.error(f"重置对话上下文失败: {e}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"重置对话上下文失败: {str(e)}" + ) \ No newline at end of file diff --git a/backend/th_agenter/api/endpoints/smart_query.py b/backend/th_agenter/api/endpoints/smart_query.py new file mode 100644 index 0000000..176f6fb --- /dev/null +++ b/backend/th_agenter/api/endpoints/smart_query.py @@ -0,0 +1,754 @@ +from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, status +from fastapi.security import HTTPBearer +from sqlalchemy.orm import Session +from typing import Optional, Dict, Any, List +import pandas as pd +from th_agenter.utils.schemas import FileListResponse,ExcelPreviewRequest,NormalResponse +import os +import tempfile +from th_agenter.utils.schemas import BaseResponse +from th_agenter.services.smart_query import ( + SmartQueryService, + ExcelAnalysisService, + DatabaseQueryService +) +from th_agenter.services.excel_metadata_service import ExcelMetadataService + +import uuid +from pathlib import Path +from th_agenter.utils.file_utils import FileUtils + +from fastapi import APIRouter, Depends, HTTPException, status +from fastapi.responses import StreamingResponse +from sqlalchemy.orm import Session +from typing import Optional, AsyncGenerator +import json +from datetime import datetime + +from th_agenter.db.database import get_db +from th_agenter.services.auth import AuthService +from th_agenter.services.smart_workflow import SmartWorkflowManager +from th_agenter.services.conversation_context import ConversationContextService +import logging +from pydantic import BaseModel + + + +logger = logging.getLogger(__name__) + +router = APIRouter(prefix="/smart-query", tags=["smart-query"]) +security = HTTPBearer() + +# Request/Response Models +class DatabaseConfig(BaseModel): + type: str + host: str + port: str + database: str + username: str + password: str + +class QueryRequest(BaseModel): + query: str + page: int = 1 + page_size: int = 20 + table_name: Optional[str] = None + +class TableSchemaRequest(BaseModel): + table_name: str + +class ExcelUploadResponse(BaseModel): + file_id: int + success: bool + message: str + data: Optional[Dict[str, Any]] = None # 添加data字段 + + +class QueryResponse(BaseModel): + success: bool + message: str + data: Optional[Dict[str, Any]] = None + + +@router.post("/upload-excel", response_model=ExcelUploadResponse) +async def upload_excel( + file: UploadFile = File(...), + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 上传Excel文件并进行预处理 + """ + try: + # 验证文件类型 + allowed_extensions = ['.xlsx', '.xls', '.csv'] + file_extension = os.path.splitext(file.filename)[1].lower() + + if file_extension not in allowed_extensions: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="不支持的文件格式,请上传 .xlsx, .xls 或 .csv 文件" + ) + + # 验证文件大小 (10MB) + content = await file.read() + file_size = len(content) + if file_size > 10 * 1024 * 1024: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="文件大小不能超过 10MB" + ) + + # 创建持久化目录结构 + backend_dir = Path(__file__).parent.parent.parent.parent # 获取backend目录 + data_dir = backend_dir / "data/uploads" + excel_user_dir = data_dir / f"excel_{current_user.id}" + + # 确保目录存在 + excel_user_dir.mkdir(parents=True, exist_ok=True) + + # 生成文件名:{uuid}_{原始文件名称} + file_id = str(uuid.uuid4()) + safe_filename = FileUtils.sanitize_filename(file.filename) + new_filename = f"{file_id}_{safe_filename}" + file_path = excel_user_dir / new_filename + + # 保存文件 + with open(file_path, 'wb') as f: + f.write(content) + + # 使用Excel元信息服务提取并保存元信息 + metadata_service = ExcelMetadataService(db) + excel_file = metadata_service.save_file_metadata( + file_path=str(file_path), + original_filename=file.filename, + user_id=current_user.id, + file_size=file_size + ) + + # 为了兼容现有前端,仍然创建pickle文件 + try: + if file_extension == '.csv': + df = pd.read_csv(file_path, encoding='utf-8') + else: + df = pd.read_excel(file_path) + except UnicodeDecodeError: + if file_extension == '.csv': + df = pd.read_csv(file_path, encoding='gbk') + else: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="文件编码错误,请确保文件为UTF-8或GBK编码" + ) + except Exception as e: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=f"文件读取失败: {str(e)}" + ) + + # 保存pickle文件到同一目录 + pickle_filename = f"{file_id}_{safe_filename}.pkl" + pickle_path = excel_user_dir / pickle_filename + df.to_pickle(pickle_path) + + # 数据预处理和分析(保持兼容性) + excel_service = ExcelAnalysisService() + analysis_result = excel_service.analyze_dataframe(df, file.filename) + + # 添加数据库文件信息 + analysis_result.update({ + 'file_id': str(excel_file.id), + 'database_id': excel_file.id, + 'temp_file_path': str(pickle_path), # 更新为新的pickle路径 + 'original_filename': file.filename, + 'file_size_mb': excel_file.file_size_mb, + 'sheet_names': excel_file.sheet_names, + }) + + return ExcelUploadResponse( + file_id=excel_file.id, + success=True, + message="Excel文件上传成功", + data=analysis_result + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"文件处理失败: {str(e)}" + ) + +@router.post("/preview-excel", response_model=QueryResponse) +async def preview_excel( + request: ExcelPreviewRequest, + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 预览Excel文件数据 + """ + try: + logger.info(f"Preview request for file_id: {request.file_id}, user: {current_user.id}") + + # 验证file_id格式 + try: + file_id = int(request.file_id) + except ValueError: + raise HTTPException( + status_code=status.HTTP_422_UNPROCESSABLE_ENTITY, + detail=f"无效的文件ID格式: {request.file_id}" + ) + + # 从数据库获取文件信息 + metadata_service = ExcelMetadataService(db) + excel_file = metadata_service.get_file_by_id(file_id, current_user.id) + + if not excel_file: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="文件不存在或已被删除" + ) + + # 检查文件是否存在 + if not os.path.exists(excel_file.file_path): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="文件已被移动或删除" + ) + + # 更新最后访问时间 + metadata_service.update_last_accessed(file_id, current_user.id) + + # 读取Excel文件 + if excel_file.file_type.lower() == 'csv': + df = pd.read_csv(excel_file.file_path, encoding='utf-8') + else: + # 对于Excel文件,使用默认sheet或第一个sheet + sheet_name = excel_file.default_sheet if excel_file.default_sheet else 0 + df = pd.read_excel(excel_file.file_path, sheet_name=sheet_name) + + # 计算分页 + total_rows = len(df) + start_idx = (request.page - 1) * request.page_size + end_idx = start_idx + request.page_size + + # 获取分页数据 + paginated_df = df.iloc[start_idx:end_idx] + + # 转换为字典格式 + data = paginated_df.fillna('').to_dict('records') + columns = df.columns.tolist() + + return QueryResponse( + success=True, + message="Excel文件预览加载成功", + data={ + 'data': data, + 'columns': columns, + 'total_rows': total_rows, + 'page': request.page, + 'page_size': request.page_size, + 'total_pages': (total_rows + request.page_size - 1) // request.page_size + } + ) + + except HTTPException: + raise + except Exception as e: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"预览文件失败: {str(e)}" + ) + +@router.post("/test-db-connection", response_model=NormalResponse) +async def test_database_connection( + config: DatabaseConfig, + current_user = Depends(AuthService.get_current_user) +): + """ + 测试数据库连接 + """ + try: + db_service = DatabaseQueryService() + is_connected = await db_service.test_connection(config.dict()) + + if is_connected: + return NormalResponse( + success=True, + message="数据库连接测试成功" + ) + else: + return NormalResponse( + success=False, + message="数据库连接测试失败" + ) + + except Exception as e: + return NormalResponse( + success=False, + message=f"连接测试失败: {str(e)}" + ) + +# 删除第285-314行的connect_database方法 +# @router.post("/connect-database", response_model=QueryResponse) +# async def connect_database( +# config_id: int, +# current_user = Depends(AuthService.get_current_user), +# db: Session = Depends(get_db) +# ): +# """连接数据库并获取表列表""" +# ... (整个方法都删除) + +@router.post("/table-schema", response_model=QueryResponse) +async def get_table_schema( + request: TableSchemaRequest, + current_user = Depends(AuthService.get_current_user) +): + """ + 获取数据表结构 + """ + try: + db_service = DatabaseQueryService() + schema_result = await db_service.get_table_schema(request.table_name, current_user.id) + + if schema_result['success']: + return QueryResponse( + success=True, + message="获取表结构成功", + data=schema_result['data'] + ) + else: + return QueryResponse( + success=False, + message=schema_result['message'] + ) + + except Exception as e: + return QueryResponse( + success=False, + message=f"获取表结构失败: {str(e)}" + ) + + +class StreamQueryRequest(BaseModel): + query: str + conversation_id: Optional[int] = None + is_new_conversation: bool = False + +class DatabaseStreamQueryRequest(BaseModel): + query: str + database_config_id: int + conversation_id: Optional[int] = None + is_new_conversation: bool = False + + +@router.post("/execute-excel-query") +async def stream_smart_query( + request: StreamQueryRequest, + current_user=Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 流式智能问答查询接口 + 支持实时推送工作流步骤和最终结果 + """ + + async def generate_stream() -> AsyncGenerator[str, None]: + workflow_manager = None + + try: + # 验证请求参数 + if not request.query or not request.query.strip(): + yield f"data: {json.dumps({'type': 'error', 'message': '查询内容不能为空'}, ensure_ascii=False)}\n\n" + return + + if len(request.query) > 1000: + yield f"data: {json.dumps({'type': 'error', 'message': '查询内容过长,请控制在1000字符以内'}, ensure_ascii=False)}\n\n" + return + + # 发送开始信号 + yield f"data: {json.dumps({'type': 'start', 'message': '开始处理查询', 'timestamp': datetime.now().isoformat()}, ensure_ascii=False)}\n\n" + + # 初始化服务 + workflow_manager = SmartWorkflowManager(db) + conversation_context_service = ConversationContextService() + + # 处理对话上下文 + conversation_id = request.conversation_id + + # 如果是新对话或没有指定对话ID,创建新对话 + if request.is_new_conversation or not conversation_id: + try: + conversation_id = await conversation_context_service.create_conversation( + user_id=current_user.id, + title=f"智能问数: {request.query[:20]}..." + ) + yield f"data: {json.dumps({'type': 'conversation_created', 'conversation_id': conversation_id}, ensure_ascii=False)}\n\n" + except Exception as e: + logger.warning(f"创建对话失败: {e}") + # 不阻断流程,继续执行查询 + + # 保存用户消息 + if conversation_id: + try: + await conversation_context_service.save_message( + conversation_id=conversation_id, + role="user", + content=request.query + ) + except Exception as e: + logger.warning(f"保存用户消息失败: {e}") + + # 执行智能查询工作流(带流式推送) + async for step_data in workflow_manager.process_excel_query_stream( + user_query=request.query, + user_id=current_user.id, + conversation_id=conversation_id, + is_new_conversation=request.is_new_conversation + ): + # 推送工作流步骤 + yield f"data: {json.dumps(step_data, ensure_ascii=False)}\n\n" + + # 如果是最终结果,保存到对话历史 + if step_data.get('type') == 'final_result' and conversation_id: + try: + result_data = step_data.get('data', {}) + await conversation_context_service.save_message( + conversation_id=conversation_id, + role="assistant", + content=result_data.get('summary', '查询完成'), + metadata={ + 'query_result': result_data, + 'workflow_steps': step_data.get('workflow_steps', []), + 'selected_files': result_data.get('used_files', []) + } + ) + + # 更新对话上下文 + await conversation_context_service.update_conversation_context( + conversation_id=conversation_id, + query=request.query, + selected_files=result_data.get('used_files', []) + ) + + logger.info(f"查询成功完成,对话ID: {conversation_id}") + + except Exception as e: + logger.warning(f"保存消息到对话历史失败: {e}") + + # 发送完成信号 + yield f"data: {json.dumps({'type': 'complete', 'message': '查询处理完成', 'timestamp': datetime.now().isoformat()}, ensure_ascii=False)}\n\n" + + except Exception as e: + logger.error(f"流式智能查询异常: {e}", exc_info=True) + yield f"data: {json.dumps({'type': 'error', 'message': f'查询执行失败: {str(e)}'}, ensure_ascii=False)}\n\n" + + finally: + # 清理资源 + if workflow_manager: + try: + workflow_manager.excel_workflow.executor.shutdown(wait=False) + except: + pass + + return StreamingResponse( + generate_stream(), + media_type="text/plain", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Content-Type": "text/event-stream", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "*", + "Access-Control-Allow-Methods": "*" + } + ) + + +@router.post("/execute-db-query") +async def execute_database_query( + request: DatabaseStreamQueryRequest, + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 流式数据库查询接口 + 支持实时推送工作流步骤和最终结果 + """ + + async def generate_stream() -> AsyncGenerator[str, None]: + workflow_manager = None + + try: + # 验证请求参数 + if not request.query or not request.query.strip(): + yield f"data: {json.dumps({'type': 'error', 'message': '查询内容不能为空'}, ensure_ascii=False)}\n\n" + return + + if len(request.query) > 1000: + yield f"data: {json.dumps({'type': 'error', 'message': '查询内容过长,请控制在1000字符以内'}, ensure_ascii=False)}\n\n" + return + + # 发送开始信号 + yield f"data: {json.dumps({'type': 'start', 'message': '开始处理数据库查询', 'timestamp': datetime.now().isoformat()}, ensure_ascii=False)}\n\n" + + # 初始化服务 + workflow_manager = SmartWorkflowManager(db) + conversation_context_service = ConversationContextService() + + # 处理对话上下文 + conversation_id = request.conversation_id + + # 如果是新对话或没有指定对话ID,创建新对话 + if request.is_new_conversation or not conversation_id: + try: + conversation_id = await conversation_context_service.create_conversation( + user_id=current_user.id, + title=f"数据库查询: {request.query[:20]}..." + ) + yield f"data: {json.dumps({'type': 'conversation_created', 'conversation_id': conversation_id}, ensure_ascii=False)}\n\n" + except Exception as e: + logger.warning(f"创建对话失败: {e}") + # 不阻断流程,继续执行查询 + + # 保存用户消息 + if conversation_id: + try: + await conversation_context_service.save_message( + conversation_id=conversation_id, + role="user", + content=request.query + ) + except Exception as e: + logger.warning(f"保存用户消息失败: {e}") + + # 执行数据库查询工作流(带流式推送) + async for step_data in workflow_manager.process_database_query_stream( + user_query=request.query, + user_id=current_user.id, + database_config_id=request.database_config_id + ): + # 推送工作流步骤 + yield f"data: {json.dumps(step_data, ensure_ascii=False)}\n\n" + + # 如果是最终结果,保存到对话历史 + if step_data.get('type') == 'final_result' and conversation_id: + try: + result_data = step_data.get('data', {}) + await conversation_context_service.save_message( + conversation_id=conversation_id, + role="assistant", + content=result_data.get('summary', '查询完成'), + metadata={ + 'query_result': result_data, + 'workflow_steps': step_data.get('workflow_steps', []), + 'generated_sql': result_data.get('generated_sql', '') + } + ) + + # 更新对话上下文 + await conversation_context_service.update_conversation_context( + conversation_id=conversation_id, + query=request.query, + selected_files=[] + ) + + logger.info(f"数据库查询成功完成,对话ID: {conversation_id}") + + except Exception as e: + logger.warning(f"保存消息到对话历史失败: {e}") + + # 发送完成信号 + yield f"data: {json.dumps({'type': 'complete', 'message': '数据库查询处理完成', 'timestamp': datetime.now().isoformat()}, ensure_ascii=False)}\n\n" + + except Exception as e: + logger.error(f"流式数据库查询异常: {e}", exc_info=True) + yield f"data: {json.dumps({'type': 'error', 'message': f'查询执行失败: {str(e)}'}, ensure_ascii=False)}\n\n" + + finally: + # 清理资源 + if workflow_manager: + try: + workflow_manager.database_workflow.executor.shutdown(wait=False) + except: + pass + + return StreamingResponse( + generate_stream(), + media_type="text/plain", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Content-Type": "text/event-stream", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "*", + "Access-Control-Allow-Methods": "*" + } + ) + +@router.delete("/cleanup-temp-files") +async def cleanup_temp_files( + current_user = Depends(AuthService.get_current_user) +): + """ + 清理临时文件 + """ + try: + temp_dir = tempfile.gettempdir() + user_prefix = f"excel_{current_user.id}_" + + cleaned_count = 0 + for filename in os.listdir(temp_dir): + if filename.startswith(user_prefix) and filename.endswith('.pkl'): + file_path = os.path.join(temp_dir, filename) + try: + os.remove(file_path) + cleaned_count += 1 + except OSError: + pass + + return BaseResponse( + success=True, + message=f"已清理 {cleaned_count} 个临时文件" + ) + + except Exception as e: + return BaseResponse( + success=False, + message=f"清理临时文件失败: {str(e)}" + ) + +@router.get("/files", response_model=FileListResponse) +async def get_file_list( + page: int = 1, + page_size: int = 20, + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 获取用户上传的Excel文件列表 + """ + try: + metadata_service = ExcelMetadataService(db) + skip = (page - 1) * page_size + files, total = metadata_service.get_user_files(current_user.id, skip, page_size) + + file_list = [] + for file in files: + file_info = { + 'id': file.id, + 'filename': file.original_filename, + 'file_size': file.file_size, + 'file_size_mb': file.file_size_mb, + 'file_type': file.file_type, + 'sheet_names': file.sheet_names, + 'sheet_count': file.sheet_count, + 'last_accessed': file.last_accessed.isoformat() if file.last_accessed else None, + 'is_processed': file.is_processed, + 'processing_error': file.processing_error + } + file_list.append(file_info) + + return FileListResponse( + success=True, + message="获取文件列表成功", + data={ + 'files': file_list, + 'total': total, + 'page': page, + 'page_size': page_size, + 'total_pages': (total + page_size - 1) // page_size + } + ) + + except Exception as e: + return FileListResponse( + success=False, + message=f"获取文件列表失败: {str(e)}" + ) + +@router.delete("/files/{file_id}", response_model=NormalResponse) +async def delete_file( + file_id: int, + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 删除指定的Excel文件 + """ + try: + metadata_service = ExcelMetadataService(db) + success = metadata_service.delete_file(file_id, current_user.id) + + if success: + return NormalResponse( + success=True, + message="文件删除成功" + ) + else: + return NormalResponse( + success=False, + message="文件不存在或删除失败" + ) + + except Exception as e: + return NormalResponse( + success=True, + message=str(e) + ) + +@router.get("/files/{file_id}/info", response_model=QueryResponse) +async def get_file_info( + file_id: int, + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """ + 获取指定文件的详细信息 + """ + try: + metadata_service = ExcelMetadataService(db) + excel_file = metadata_service.get_file_by_id(file_id, current_user.id) + + if not excel_file: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="文件不存在" + ) + + # 更新最后访问时间 + metadata_service.update_last_accessed(file_id, current_user.id) + + file_info = { + 'id': excel_file.id, + 'filename': excel_file.original_filename, + 'file_size': excel_file.file_size, + 'file_size_mb': excel_file.file_size_mb, + 'file_type': excel_file.file_type, + 'sheet_names': excel_file.sheet_names, + 'default_sheet': excel_file.default_sheet, + 'columns_info': excel_file.columns_info, + 'preview_data': excel_file.preview_data, + 'data_types': excel_file.data_types, + 'total_rows': excel_file.total_rows, + 'total_columns': excel_file.total_columns, + 'upload_time': excel_file.upload_time.isoformat() if excel_file.upload_time else None, + 'last_accessed': excel_file.last_accessed.isoformat() if excel_file.last_accessed else None, + 'sheets_summary': excel_file.get_all_sheets_summary() + } + + return QueryResponse( + success=True, + message="获取文件信息成功", + data=file_info + ) + + except HTTPException: + raise + except Exception as e: + return QueryResponse( + success=False, + message=f"获取文件信息失败: {str(e)}" + ) \ No newline at end of file diff --git a/backend/th_agenter/api/endpoints/table_metadata.py b/backend/th_agenter/api/endpoints/table_metadata.py new file mode 100644 index 0000000..95b8785 --- /dev/null +++ b/backend/th_agenter/api/endpoints/table_metadata.py @@ -0,0 +1,248 @@ +"""表元数据管理API""" + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.orm import Session +from typing import List, Dict, Any +from pydantic import BaseModel, Field + +from th_agenter.models.user import User +from th_agenter.db.database import get_db +from th_agenter.services.table_metadata_service import TableMetadataService +from th_agenter.utils.logger import get_logger +from th_agenter.services.auth import AuthService + +logger = get_logger("table_metadata_api") +router = APIRouter(prefix="/api/table-metadata", tags=["table-metadata"]) + + +class TableSelectionRequest(BaseModel): + database_config_id: int = Field(..., description="数据库配置ID") + table_names: List[str] = Field(..., description="选中的表名列表") + + +class TableMetadataResponse(BaseModel): + id: int + table_name: str + table_schema: str + table_type: str + table_comment: str + columns_count: int + row_count: int + is_enabled_for_qa: bool + qa_description: str + business_context: str + last_synced_at: str + + +class QASettingsUpdate(BaseModel): + is_enabled_for_qa: bool = Field(default=True) + qa_description: str = Field(default="") + business_context: str = Field(default="") + + +class TableByNameRequest(BaseModel): + database_config_id: int = Field(..., description="数据库配置ID") + table_name: str = Field(..., description="表名") + + +@router.post("/collect") +async def collect_table_metadata( + request: TableSelectionRequest, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """收集选中表的元数据""" + try: + service = TableMetadataService(db) + result = await service.collect_and_save_table_metadata( + current_user.id, + request.database_config_id, + request.table_names + ) + return result + except Exception as e: + logger.error(f"收集表元数据失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + + +@router.get("/") +async def get_table_metadata( + database_config_id: int = None, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """获取表元数据列表""" + try: + service = TableMetadataService(db) + metadata_list = service.get_user_table_metadata( + current_user.id, + database_config_id + ) + + data = [ + { + "id": meta.id, + "table_name": meta.table_name, + "table_schema": meta.table_schema, + "table_type": meta.table_type, + "table_comment": meta.table_comment or "", + "columns": meta.columns_info if meta.columns_info else [], + "column_count": len(meta.columns_info) if meta.columns_info else 0, + "row_count": meta.row_count, + "is_enabled_for_qa": meta.is_enabled_for_qa, + "qa_description": meta.qa_description or "", + "business_context": meta.business_context or "", + "created_at": meta.created_at.isoformat() if meta.created_at else "", + "updated_at": meta.updated_at.isoformat() if meta.updated_at else "", + "last_synced_at": meta.last_synced_at.isoformat() if meta.last_synced_at else "", + "qa_settings": { + "is_enabled_for_qa": meta.is_enabled_for_qa, + "qa_description": meta.qa_description or "", + "business_context": meta.business_context or "" + } + } + for meta in metadata_list + ] + + return { + "success": True, + "data": data + } + + except Exception as e: + logger.error(f"获取表元数据失败: {str(e)}") + return { + "success": False, + "message": str(e) + } + + +@router.post("/by-table") +async def get_table_metadata_by_name( + request: TableByNameRequest, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """根据表名获取表元数据""" + try: + service = TableMetadataService(db) + metadata = service.get_table_metadata_by_name( + current_user.id, + request.database_config_id, + request.table_name + ) + + if metadata: + data = { + "id": metadata.id, + "table_name": metadata.table_name, + "table_schema": metadata.table_schema, + "table_type": metadata.table_type, + "table_comment": metadata.table_comment or "", + "columns": metadata.columns_info if metadata.columns_info else [], + "column_count": len(metadata.columns_info) if metadata.columns_info else 0, + "row_count": metadata.row_count, + "is_enabled_for_qa": metadata.is_enabled_for_qa, + "qa_description": metadata.qa_description or "", + "business_context": metadata.business_context or "", + "created_at": metadata.created_at.isoformat() if metadata.created_at else "", + "updated_at": metadata.updated_at.isoformat() if metadata.updated_at else "", + "last_synced_at": metadata.last_synced_at.isoformat() if metadata.last_synced_at else "", + "qa_settings": { + "is_enabled_for_qa": metadata.is_enabled_for_qa, + "qa_description": metadata.qa_description or "", + "business_context": metadata.business_context or "" + } + } + return {"success": True, "data": data} + else: + return {"success": False, "data": None, "message": "表元数据不存在"} + + except Exception as e: + logger.error(f"获取表元数据失败: {str(e)}") + return { + "success": False, + "message": str(e) + } + + return { + "success": True, + "data": data + } + except Exception as e: + logger.error(f"获取表元数据失败: {str(e)}") + return { + "success": False, + "message": str(e) + } + + +@router.put("/{metadata_id}/qa-settings") +async def update_qa_settings( + metadata_id: int, + settings: QASettingsUpdate, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """更新表的问答设置""" + try: + service = TableMetadataService(db) + success = service.update_table_qa_settings( + current_user.id, + metadata_id, + settings.dict() + ) + + if success: + return {"success": True, "message": "设置更新成功"} + else: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="表元数据不存在" + ) + except Exception as e: + logger.error(f"更新问答设置失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail=str(e) + ) + + +class TableSaveRequest(BaseModel): + database_config_id: int = Field(..., description="数据库配置ID") + table_names: List[str] = Field(..., description="要保存的表名列表") + + +@router.post("/save") +async def save_table_metadata( + request: TableSaveRequest, + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """保存选中表的元数据配置""" + try: + service = TableMetadataService(db) + result = await service.save_table_metadata_config( + user_id=current_user.id, + database_config_id=request.database_config_id, + table_names=request.table_names + ) + + logger.info(f"用户 {current_user.id} 保存了 {len(request.table_names)} 个表的配置") + + return { + "success": True, + "message": f"成功保存 {len(result['saved_tables'])} 个表的配置", + "saved_tables": result['saved_tables'], + "failed_tables": result.get('failed_tables', []) + } + + except Exception as e: + logger.error(f"保存表元数据配置失败: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"保存配置失败: {str(e)}" + ) \ No newline at end of file diff --git a/backend/th_agenter/api/endpoints/users.py b/backend/th_agenter/api/endpoints/users.py new file mode 100644 index 0000000..76e01bf --- /dev/null +++ b/backend/th_agenter/api/endpoints/users.py @@ -0,0 +1,241 @@ +"""User management endpoints.""" + +from typing import List, Optional +from fastapi import APIRouter, Depends, HTTPException, status, Query +from sqlalchemy.orm import Session + +from ...db.database import get_db +from ...core.simple_permissions import require_super_admin +from ...services.auth import AuthService +from ...services.user import UserService +from ...schemas.user import UserResponse, UserUpdate, UserCreate, ChangePasswordRequest, ResetPasswordRequest + +router = APIRouter() + + +@router.get("/profile", response_model=UserResponse) +async def get_user_profile( + current_user = Depends(AuthService.get_current_user) +): + """Get current user profile.""" + return UserResponse.from_orm(current_user) + + +@router.put("/profile", response_model=UserResponse) +async def update_user_profile( + user_update: UserUpdate, + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """Update current user profile.""" + user_service = UserService(db) + + # Check if email is being changed and is already taken + if user_update.email and user_update.email != current_user.email: + existing_user = user_service.get_user_by_email(user_update.email) + if existing_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Email already registered" + ) + + # Update user + updated_user = user_service.update_user(current_user.id, user_update) + return UserResponse.from_orm(updated_user) + + +@router.delete("/profile") +async def delete_user_account( + current_user = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +): + """Delete current user account.""" + user_service = UserService(db) + user_service.delete_user(current_user.id) + return {"message": "Account deleted successfully"} + + +# Admin endpoints +@router.post("/", response_model=UserResponse) +async def create_user( + user_create: UserCreate, + # current_user = Depends(require_superuser), + db: Session = Depends(get_db) +): + """Create a new user (admin only).""" + user_service = UserService(db) + + # Check if username already exists + existing_user = user_service.get_user_by_username(user_create.username) + if existing_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Username already registered" + ) + + # Check if email already exists + existing_user = user_service.get_user_by_email(user_create.email) + if existing_user: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Email already registered" + ) + + # Create user + new_user = user_service.create_user(user_create) + return UserResponse.from_orm(new_user) + + +@router.get("/") +async def list_users( + page: int = Query(1, ge=1), + size: int = Query(20, ge=1, le=100), + search: Optional[str] = Query(None), + role_id: Optional[int] = Query(None), + is_active: Optional[bool] = Query(None), + # current_user = Depends(require_superuser), + db: Session = Depends(get_db) +): + """List all users with pagination and filters (admin only).""" + user_service = UserService(db) + skip = (page - 1) * size + users, total = user_service.get_users_with_filters( + skip=skip, + limit=size, + search=search, + role_id=role_id, + is_active=is_active + ) + result = { + "users": [UserResponse.from_orm(user) for user in users], + "total": total, + "page": page, + "page_size": size + } + return result + + +@router.get("/{user_id}", response_model=UserResponse) +async def get_user( + user_id: int, + current_user = Depends(AuthService.get_current_active_user), + db: Session = Depends(get_db) +): + """Get user by ID (admin only).""" + user_service = UserService(db) + user = user_service.get_user(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + return UserResponse.from_orm(user) + + +@router.put("/change-password") +async def change_password( + request: ChangePasswordRequest, + current_user = Depends(AuthService.get_current_active_user), + db: Session = Depends(get_db) +): + """Change current user's password.""" + user_service = UserService(db) + + try: + user_service.change_password( + user_id=current_user.id, + current_password=request.current_password, + new_password=request.new_password + ) + return {"message": "Password changed successfully"} + except Exception as e: + if "Current password is incorrect" in str(e): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Current password is incorrect" + ) + elif "must be at least 6 characters" in str(e): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="New password must be at least 6 characters long" + ) + else: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to change password" + ) + + +@router.put("/{user_id}/reset-password") +async def reset_user_password( + user_id: int, + request: ResetPasswordRequest, + current_user = Depends(require_super_admin), + db: Session = Depends(get_db) +): + """Reset user password (admin only).""" + user_service = UserService(db) + + try: + user_service.reset_password( + user_id=user_id, + new_password=request.new_password + ) + return {"message": "Password reset successfully"} + except Exception as e: + if "User not found" in str(e): + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + elif "must be at least 6 characters" in str(e): + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="New password must be at least 6 characters long" + ) + else: + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="Failed to reset password" + ) + + +@router.put("/{user_id}", response_model=UserResponse) +async def update_user( + user_id: int, + user_update: UserUpdate, + current_user = Depends(AuthService.get_current_active_user), + db: Session = Depends(get_db) +): + """Update user by ID (admin only).""" + user_service = UserService(db) + + user = user_service.get_user(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + updated_user = user_service.update_user(user_id, user_update) + return UserResponse.from_orm(updated_user) + + +@router.delete("/{user_id}") +async def delete_user( + user_id: int, + current_user = Depends(AuthService.get_current_active_user), + db: Session = Depends(get_db) +): + """Delete user by ID (admin only).""" + user_service = UserService(db) + + user = user_service.get_user(user_id) + if not user: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="User not found" + ) + + user_service.delete_user(user_id) + return {"message": "User deleted successfully"} \ No newline at end of file diff --git a/backend/th_agenter/api/endpoints/workflow.py b/backend/th_agenter/api/endpoints/workflow.py new file mode 100644 index 0000000..9fd7f54 --- /dev/null +++ b/backend/th_agenter/api/endpoints/workflow.py @@ -0,0 +1,538 @@ +"""工作流管理API""" + +from typing import List, Optional, AsyncGenerator +from fastapi import APIRouter, Depends, HTTPException, status, Query +from fastapi.responses import StreamingResponse +from sqlalchemy.orm import Session +from sqlalchemy import and_ +import json +from datetime import datetime + +from ...db.database import get_db +from ...schemas.workflow import ( + WorkflowCreate, WorkflowUpdate, WorkflowResponse, WorkflowListResponse, + WorkflowExecuteRequest, WorkflowExecutionResponse, NodeExecutionResponse, WorkflowStatus +) +from ...models.workflow import WorkflowStatus as ModelWorkflowStatus +from ...services.workflow_engine import get_workflow_engine +from ...services.auth import AuthService +from ...models.user import User +from ...utils.logger import get_logger + +logger = get_logger("workflow_api") + +router = APIRouter() + +def convert_workflow_for_response(workflow_dict): + """转换工作流数据以适配响应模型""" + if workflow_dict.get('definition') and workflow_dict['definition'].get('connections'): + for conn in workflow_dict['definition']['connections']: + if 'from_node' in conn: + conn['from'] = conn.pop('from_node') + if 'to_node' in conn: + conn['to'] = conn.pop('to_node') + return workflow_dict + +@router.post("/", response_model=WorkflowResponse) +async def create_workflow( + workflow_data: WorkflowCreate, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """创建工作流""" + try: + from ...models.workflow import Workflow + + # 创建工作流 + workflow = Workflow( + name=workflow_data.name, + description=workflow_data.description, + definition=workflow_data.definition.dict(), + version="1.0.0", + status=workflow_data.status, + owner_id=current_user.id + ) + workflow.set_audit_fields(current_user.id) + + db.add(workflow) + db.commit() + db.refresh(workflow) + + # 转换definition中的字段映射 + workflow_dict = convert_workflow_for_response(workflow.to_dict()) + + logger.info(f"Created workflow: {workflow.name} by user {current_user.username}") + return WorkflowResponse(**workflow_dict) + + except Exception as e: + db.rollback() + logger.error(f"Error creating workflow: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="创建工作流失败" + ) + +@router.get("/", response_model=WorkflowListResponse) +async def list_workflows( + skip: Optional[int] = Query(None, ge=0), + limit: Optional[int] = Query(None, ge=1, le=100), + workflow_status: Optional[WorkflowStatus] = None, + search: Optional[str] = Query(None), + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """获取工作流列表""" + try: + from ...models.workflow import Workflow + + # 构建查询 + query = db.query(Workflow).filter(Workflow.owner_id == current_user.id) + + if workflow_status: + query = query.filter(Workflow.status == workflow_status) + + # 添加搜索功能 + if search: + query = query.filter(Workflow.name.ilike(f"%{search}%")) + + # 获取总数 + total = query.count() + + # 如果没有传分页参数,返回所有数据 + if skip is None and limit is None: + workflows = query.all() + return WorkflowListResponse( + workflows=[WorkflowResponse(**convert_workflow_for_response(w.to_dict())) for w in workflows], + total=total, + page=1, + size=total + ) + + # 使用默认分页参数 + if skip is None: + skip = 0 + if limit is None: + limit = 10 + + # 分页查询 + workflows = query.offset(skip).limit(limit).all() + + return WorkflowListResponse( + workflows=[WorkflowResponse(**convert_workflow_for_response(w.to_dict())) for w in workflows], + total=total, + page=skip // limit + 1, # 计算页码 + size=limit + ) + + except Exception as e: + logger.error(f"Error listing workflows: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取工作流列表失败" + ) + +@router.get("/{workflow_id}", response_model=WorkflowResponse) +async def get_workflow( + workflow_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """获取工作流详情""" + try: + from ...models.workflow import Workflow + + workflow = db.query(Workflow).filter( + and_( + Workflow.id == workflow_id, + Workflow.owner_id == current_user.id + ) + ).first() + + if not workflow: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="工作流不存在" + ) + + return WorkflowResponse(**convert_workflow_for_response(workflow.to_dict())) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error getting workflow {workflow_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取工作流失败" + ) + +@router.put("/{workflow_id}", response_model=WorkflowResponse) +async def update_workflow( + workflow_id: int, + workflow_data: WorkflowUpdate, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """更新工作流""" + try: + from ...models.workflow import Workflow + + workflow = db.query(Workflow).filter( + and_( + Workflow.id == workflow_id, + Workflow.owner_id == current_user.id + ) + ).first() + + if not workflow: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="工作流不存在" + ) + + # 更新字段 + update_data = workflow_data.dict(exclude_unset=True) + for field, value in update_data.items(): + if field == "definition" and value: + # 如果value是Pydantic模型,转换为字典;如果已经是字典,直接使用 + if hasattr(value, 'dict'): + setattr(workflow, field, value.dict()) + else: + setattr(workflow, field, value) + else: + setattr(workflow, field, value) + + workflow.set_audit_fields(current_user.id, is_update=True) + + db.commit() + db.refresh(workflow) + + logger.info(f"Updated workflow: {workflow.name} by user {current_user.username}") + return WorkflowResponse(**convert_workflow_for_response(workflow.to_dict())) + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error updating workflow {workflow_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="更新工作流失败" + ) + +@router.delete("/{workflow_id}") +async def delete_workflow( + workflow_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """删除工作流""" + try: + from ...models.workflow import Workflow + + workflow = db.query(Workflow).filter( + and_( + Workflow.id == workflow_id, + Workflow.owner_id == current_user.id + ) + ).first() + + if not workflow: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="工作流不存在" + ) + + db.delete(workflow) + db.commit() + + logger.info(f"Deleted workflow: {workflow.name} by user {current_user.username}") + return {"message": "工作流删除成功"} + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error deleting workflow {workflow_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="删除工作流失败" + ) + +@router.post("/{workflow_id}/activate") +async def activate_workflow( + workflow_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """激活工作流""" + try: + from ...models.workflow import Workflow + + workflow = db.query(Workflow).filter( + and_( + Workflow.id == workflow_id, + Workflow.owner_id == current_user.id + ) + ).first() + + if not workflow: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="工作流不存在" + ) + + workflow.status = ModelWorkflowStatus.PUBLISHED + workflow.set_audit_fields(current_user.id, is_update=True) + + db.commit() + + logger.info(f"Activated workflow: {workflow.name} by user {current_user.username}") + return {"message": "工作流激活成功"} + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error activating workflow {workflow_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="激活工作流失败" + ) + +@router.post("/{workflow_id}/deactivate") +async def deactivate_workflow( + workflow_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """停用工作流""" + try: + from ...models.workflow import Workflow + + workflow = db.query(Workflow).filter( + and_( + Workflow.id == workflow_id, + Workflow.owner_id == current_user.id + ) + ).first() + + if not workflow: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="工作流不存在" + ) + + workflow.status = ModelWorkflowStatus.ARCHIVED + workflow.set_audit_fields(current_user.id, is_update=True) + + db.commit() + + logger.info(f"Deactivated workflow: {workflow.name} by user {current_user.username}") + return {"message": "工作流停用成功"} + + except HTTPException: + raise + except Exception as e: + db.rollback() + logger.error(f"Error deactivating workflow {workflow_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="停用工作流失败" + ) + +@router.post("/{workflow_id}/execute", response_model=WorkflowExecutionResponse) +async def execute_workflow( + workflow_id: int, + request: WorkflowExecuteRequest, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """执行工作流""" + try: + from ...models.workflow import Workflow + + workflow = db.query(Workflow).filter( + and_( + Workflow.id == workflow_id, + Workflow.owner_id == current_user.id + ) + ).first() + + if not workflow: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="工作流不存在" + ) + + if workflow.status != ModelWorkflowStatus.PUBLISHED: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="工作流未激活,无法执行" + ) + + # 获取工作流引擎并执行 + engine = get_workflow_engine() + execution_result = await engine.execute_workflow( + workflow=workflow, + input_data=request.input_data, + user_id=current_user.id, + db=db + ) + + logger.info(f"Executed workflow: {workflow.name} by user {current_user.username}") + return execution_result + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error executing workflow {workflow_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail=f"执行工作流失败: {str(e)}" + ) + +@router.get("/{workflow_id}/executions", response_model=List[WorkflowExecutionResponse]) +async def list_workflow_executions( + workflow_id: int, + skip: int = Query(0, ge=0), + limit: int = Query(10, ge=1, le=100), + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """获取工作流执行历史""" + try: + from ...models.workflow import Workflow, WorkflowExecution + + # 验证工作流所有权 + workflow = db.query(Workflow).filter( + and_( + Workflow.id == workflow_id, + Workflow.owner_id == current_user.id + ) + ).first() + + if not workflow: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="工作流不存在" + ) + + # 获取执行历史 + executions = db.query(WorkflowExecution).filter( + WorkflowExecution.workflow_id == workflow_id + ).order_by(WorkflowExecution.created_at.desc()).offset(skip).limit(limit).all() + + return [WorkflowExecutionResponse.from_orm(execution) for execution in executions] + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error listing workflow executions {workflow_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取执行历史失败" + ) + +@router.get("/executions/{execution_id}", response_model=WorkflowExecutionResponse) +async def get_workflow_execution( + execution_id: int, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """获取工作流执行详情""" + try: + from ...models.workflow import WorkflowExecution, Workflow + + execution = db.query(WorkflowExecution).join( + Workflow, WorkflowExecution.workflow_id == Workflow.id + ).filter( + and_( + WorkflowExecution.id == execution_id, + Workflow.owner_id == current_user.id + ) + ).first() + + if not execution: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail="执行记录不存在" + ) + + return WorkflowExecutionResponse.from_orm(execution) + + except HTTPException: + raise + except Exception as e: + logger.error(f"Error getting workflow execution {execution_id}: {str(e)}") + raise HTTPException( + status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, + detail="获取执行详情失败" + ) + + +@router.post("/{workflow_id}/execute-stream") +async def execute_workflow_stream( + workflow_id: int, + request: WorkflowExecuteRequest, + db: Session = Depends(get_db), + current_user: User = Depends(AuthService.get_current_user) +): + """流式执行工作流,实时推送节点执行状态""" + + async def generate_stream() -> AsyncGenerator[str, None]: + workflow_engine = None + + try: + from ...models.workflow import Workflow + + # 验证工作流 + workflow = db.query(Workflow).filter( + and_( + Workflow.id == workflow_id, + Workflow.owner_id == current_user.id + ) + ).first() + + if not workflow: + yield f"data: {json.dumps({'type': 'error', 'message': '工作流不存在'}, ensure_ascii=False)}\n\n" + return + + if workflow.status != ModelWorkflowStatus.PUBLISHED: + yield f"data: {json.dumps({'type': 'error', 'message': '工作流未激活,无法执行'}, ensure_ascii=False)}\n\n" + return + + # 发送开始信号 + yield f"data: {json.dumps({'type': 'workflow_start', 'workflow_id': workflow_id, 'workflow_name': workflow.name, 'timestamp': datetime.now().isoformat()}, ensure_ascii=False)}\n\n" + + # 获取工作流引擎 + workflow_engine = get_workflow_engine() + + # 执行工作流(流式版本) + async for step_data in workflow_engine.execute_workflow_stream( + workflow=workflow, + input_data=request.input_data, + user_id=current_user.id, + db=db + ): + # 推送工作流步骤 + yield f"data: {json.dumps(step_data, ensure_ascii=False)}\n\n" + + # 发送完成信号 + yield f"data: {json.dumps({'type': 'workflow_complete', 'message': '工作流执行完成', 'timestamp': datetime.now().isoformat()}, ensure_ascii=False)}\n\n" + + except Exception as e: + logger.error(f"流式工作流执行异常: {e}", exc_info=True) + yield f"data: {json.dumps({'type': 'error', 'message': f'工作流执行失败: {str(e)}'}, ensure_ascii=False)}\n\n" + + return StreamingResponse( + generate_stream(), + media_type="text/plain", + headers={ + "Cache-Control": "no-cache", + "Connection": "keep-alive", + "Content-Type": "text/event-stream", + "Access-Control-Allow-Origin": "*", + "Access-Control-Allow-Headers": "*", + "Access-Control-Allow-Methods": "*" + } + ) \ No newline at end of file diff --git a/backend/th_agenter/api/routes.py b/backend/th_agenter/api/routes.py new file mode 100644 index 0000000..d5dcece --- /dev/null +++ b/backend/th_agenter/api/routes.py @@ -0,0 +1,101 @@ +"""Main API router.""" + +from fastapi import APIRouter +from .endpoints import chat + + +# TODO: Add other routers when implemented +from .endpoints import auth +from .endpoints import knowledge_base +from .endpoints import smart_query +from .endpoints import smart_chat + +from .endpoints import database_config +from .endpoints import table_metadata + +# System management endpoints +from .endpoints import roles +from .endpoints import llm_configs +from .endpoints import users + +# Workflow endpoints +from .endpoints import workflow + + +# Create main API router +router = APIRouter() + +router.include_router( + auth.router, + prefix="/auth", + tags=["authentication"] +) + +# Include sub-routers +router.include_router( + chat.router, + prefix="/chat", + tags=["chat"] +) + + + +router.include_router( + knowledge_base.router, + prefix="/knowledge-bases", + tags=["knowledge-bases"] +) + +router.include_router( + smart_query.router, + tags=["smart-query"] +) + +router.include_router( + smart_chat.router, + tags=["smart-chat"] +) + + + + + +router.include_router( + database_config.router, + tags=["database-config"] +) + +router.include_router( + table_metadata.router, + tags=["table-metadata"] +) + +# System management routers +router.include_router( + roles.router, + prefix="/admin", + tags=["admin-roles"] +) + +router.include_router( + llm_configs.router, + prefix="/admin", + tags=["admin-llm-configs"] +) + +router.include_router( + users.router, + prefix="/users", + tags=["users"] +) + +router.include_router( + workflow.router, + prefix="/workflows", + tags=["workflows"] +) + +# Test endpoint +@router.get("/test") +async def test_endpoint(): + return {"message": "API test is working"} \ No newline at end of file diff --git a/backend/th_agenter/core/__init__.py b/backend/th_agenter/core/__init__.py new file mode 100644 index 0000000..cacaf5f --- /dev/null +++ b/backend/th_agenter/core/__init__.py @@ -0,0 +1 @@ +"""Core module for TH-Agenter.""" \ No newline at end of file diff --git a/backend/th_agenter/core/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/core/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..91dca0d4b9307bd854d08214b55e0e56f373c40a GIT binary patch literal 203 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 G<^Km}L6tZF literal 0 HcmV?d00001 diff --git a/backend/th_agenter/core/__pycache__/config.cpython-313.pyc b/backend/th_agenter/core/__pycache__/config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..86e4932d9f5606b43fa4344302584409523236fd GIT binary patch literal 21359 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 M$2EUVux79NA7gt16#xJL literal 0 HcmV?d00001 diff --git a/backend/th_agenter/core/__pycache__/llm.cpython-313.pyc b/backend/th_agenter/core/__pycache__/llm.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..54dedc0cc74231011015dc90e76868a5a7cdcea8 GIT binary patch literal 2601 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 ZZs@;gYvy8W1js-4id>}91cZIce*nWw+CKmQ literal 0 HcmV?d00001 diff --git a/backend/th_agenter/core/app.py b/backend/th_agenter/core/app.py new file mode 100644 index 0000000..33cbd4e --- /dev/null +++ b/backend/th_agenter/core/app.py @@ -0,0 +1,177 @@ +"""FastAPI application factory.""" + +import logging +from contextlib import asynccontextmanager +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +from fastapi.middleware.trustedhost import TrustedHostMiddleware +from fastapi.responses import JSONResponse +from fastapi.exceptions import RequestValidationError +from starlette.exceptions import HTTPException as StarletteHTTPException + +from .config import Settings +from .logging import setup_logging +from .middleware import UserContextMiddleware +from ..api.routes import router +from ..db.database import init_db +from ..api.endpoints import table_metadata + + +@asynccontextmanager +async def lifespan(app: FastAPI): + """Application lifespan manager.""" + # Startup + logging.info("Starting up TH-Agenter application...") + await init_db() + logging.info("Database initialized") + + yield + + # Shutdown + logging.info("Shutting down TH-Agenter application...") + + +def create_app(settings: Settings = None) -> FastAPI: + """Create and configure FastAPI application.""" + if settings is None: + from .config import get_settings + settings = get_settings() + + # Setup logging + setup_logging(settings.logging) + + # Create FastAPI app + app = FastAPI( + title=settings.app_name, + version=settings.app_version, + description="A modern chat agent application with Vue frontend and FastAPI backend", + debug=settings.debug, + lifespan=lifespan, + ) + + # Add middleware + setup_middleware(app, settings) + + # Add exception handlers + setup_exception_handlers(app) + + # Include routers + app.include_router(router, prefix="/api") + + + + app.include_router(table_metadata.router) + # 在现有导入中添加 + from ..api.endpoints import database_config + + # 在路由注册部分添加 + app.include_router(database_config.router) + # Health check endpoint + @app.get("/health") + async def health_check(): + return {"status": "healthy", "version": settings.app_version} + + # Root endpoint + @app.get("/") + async def root(): + return {"message": "Chat Agent API is running"} + + # Test endpoint + @app.get("/test") + async def test_endpoint(): + return {"message": "API is working"} + + return app + + +def setup_middleware(app: FastAPI, settings: Settings) -> None: + """Setup application middleware.""" + + # User context middleware (should be first to set context for all requests) + app.add_middleware(UserContextMiddleware) + + # CORS middleware + app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors.allowed_origins, + allow_credentials=True, + allow_methods=settings.cors.allowed_methods, + allow_headers=settings.cors.allowed_headers, + ) + + # Trusted host middleware (for production) + if settings.environment == "production": + app.add_middleware( + TrustedHostMiddleware, + allowed_hosts=["*"] # Configure this properly in production + ) + + +def setup_exception_handlers(app: FastAPI) -> None: + """Setup global exception handlers.""" + + @app.exception_handler(StarletteHTTPException) + async def http_exception_handler(request, exc): + return JSONResponse( + status_code=exc.status_code, + content={ + "error": { + "type": "http_error", + "message": exc.detail, + "status_code": exc.status_code + } + } + ) + + def make_json_serializable(obj): + """递归地将对象转换为JSON可序列化的格式""" + if obj is None or isinstance(obj, (str, int, float, bool)): + return obj + elif isinstance(obj, bytes): + return obj.decode('utf-8') + elif isinstance(obj, (ValueError, Exception)): + return str(obj) + elif isinstance(obj, dict): + return {k: make_json_serializable(v) for k, v in obj.items()} + elif isinstance(obj, (list, tuple)): + return [make_json_serializable(item) for item in obj] + else: + # For any other object, convert to string + return str(obj) + + @app.exception_handler(RequestValidationError) + async def validation_exception_handler(request, exc): + # Convert any non-serializable objects to strings in error details + try: + errors = make_json_serializable(exc.errors()) + except Exception as e: + # Fallback: if even our conversion fails, use a simple error message + errors = [{"type": "serialization_error", "msg": f"Error processing validation details: {str(e)}"}] + + return JSONResponse( + status_code=422, + content={ + "error": { + "type": "validation_error", + "message": "Request validation failed", + "details": errors + } + } + ) + + @app.exception_handler(Exception) + async def general_exception_handler(request, exc): + logging.error(f"Unhandled exception: {exc}", exc_info=True) + return JSONResponse( + status_code=500, + content={ + "error": { + "type": "internal_error", + "message": "Internal server error" + } + } + ) + + +# Create the app instance +app = create_app() \ No newline at end of file diff --git a/backend/th_agenter/core/config.py b/backend/th_agenter/core/config.py new file mode 100644 index 0000000..dc76123 --- /dev/null +++ b/backend/th_agenter/core/config.py @@ -0,0 +1,482 @@ +"""Configuration management for TH-Agenter.""" + +import os +import yaml +from pathlib import Path +from typing import Any, Dict, List, Optional, Union +from pydantic import Field, field_validator +from pydantic_settings import BaseSettings +from functools import lru_cache + + +class DatabaseSettings(BaseSettings): + """Database configuration.""" + url: str = Field(..., alias="database_url") # Must be provided via environment variable + echo: bool = Field(default=False) + pool_size: int = Field(default=5) + max_overflow: int = Field(default=10) + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + + +class SecuritySettings(BaseSettings): + """Security configuration.""" + secret_key: str = Field(default="your-secret-key-here-change-in-production") + algorithm: str = Field(default="HS256") + access_token_expire_minutes: int = Field(default=300) + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + +class ToolSetings(BaseSettings): + # Tavily搜索配置 + tavily_api_key: Optional[str] = Field(default=None) + weather_api_key: Optional[str] = Field(default=None) + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } +class LLMSettings(BaseSettings): + """大模型配置 - 支持多种OpenAI协议兼容的服务商.""" + provider: str = Field(default="openai", alias="llm_provider") # openai, deepseek, doubao, zhipu, moonshot + + # OpenAI配置 + openai_api_key: Optional[str] = Field(default=None) + openai_base_url: str = Field(default="https://api.openai.com/v1") + openai_model: str = Field(default="gpt-3.5-turbo") + + # DeepSeek配置 + deepseek_api_key: Optional[str] = Field(default=None) + deepseek_base_url: str = Field(default="https://api.deepseek.com/v1") + deepseek_model: str = Field(default="deepseek-chat") + + # 豆包配置 + doubao_api_key: Optional[str] = Field(default=None) + doubao_base_url: str = Field(default="https://ark.cn-beijing.volces.com/api/v3") + doubao_model: str = Field(default="doubao-lite-4k") + + # 智谱AI配置 + zhipu_api_key: Optional[str] = Field(default=None) + zhipu_base_url: str = Field(default="https://open.bigmodel.cn/api/paas/v4") + zhipu_model: str = Field(default="glm-4") + zhipu_embedding_model: str = Field(default="embedding-3") + + # 月之暗面配置 + moonshot_api_key: Optional[str] = Field(default=None) + moonshot_base_url: str = Field(default="https://api.moonshot.cn/v1") + moonshot_model: str = Field(default="moonshot-v1-8k") + + # 通用配置 + max_tokens: int = Field(default=2048) + temperature: float = Field(default=0.7) + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + + def get_current_config(self) -> dict: + """获取当前选择的提供商配置 - 优先从数据库读取默认配置.""" + try: + # 尝试从数据库读取默认聊天模型配置 + from th_agenter.services.llm_config_service import LLMConfigService + llm_service = LLMConfigService() + db_config = llm_service.get_default_chat_config() + + if db_config: + # 如果数据库中有默认配置,使用数据库配置 + config = { + "api_key": db_config.api_key, + "base_url": db_config.base_url, + "model": db_config.model_name, + "max_tokens": self.max_tokens, + "temperature": self.temperature + } + return config + except Exception as e: + # 如果数据库读取失败,记录错误并回退到环境变量 + import logging + logging.warning(f"Failed to read LLM config from database, falling back to env vars: {e}") + + # 回退到原有的环境变量配置 + provider_configs = { + "openai": { + "api_key": self.openai_api_key, + "base_url": self.openai_base_url, + "model": self.openai_model + }, + "deepseek": { + "api_key": self.deepseek_api_key, + "base_url": self.deepseek_base_url, + "model": self.deepseek_model + }, + "doubao": { + "api_key": self.doubao_api_key, + "base_url": self.doubao_base_url, + "model": self.doubao_model + }, + "zhipu": { + "api_key": self.zhipu_api_key, + "base_url": self.zhipu_base_url, + "model": self.zhipu_model + }, + "moonshot": { + "api_key": self.moonshot_api_key, + "base_url": self.moonshot_base_url, + "model": self.moonshot_model + } + } + + config = provider_configs.get(self.provider, provider_configs["openai"]) + config.update({ + "max_tokens": self.max_tokens, + "temperature": self.temperature + }) + return config + + +class EmbeddingSettings(BaseSettings): + """Embedding模型配置 - 支持多种提供商.""" + provider: str = Field(default="zhipu", alias="embedding_provider") # openai, deepseek, doubao, zhipu, moonshot + + # OpenAI配置 + openai_api_key: Optional[str] = Field(default=None) + openai_base_url: str = Field(default="https://api.openai.com/v1") + openai_embedding_model: str = Field(default="text-embedding-ada-002") + + # DeepSeek配置 + deepseek_api_key: Optional[str] = Field(default=None) + deepseek_base_url: str = Field(default="https://api.deepseek.com/v1") + deepseek_embedding_model: str = Field(default="deepseek-embedding") + + # 豆包配置 + doubao_api_key: Optional[str] = Field(default=None) + doubao_base_url: str = Field(default="https://ark.cn-beijing.volces.com/api/v3") + doubao_embedding_model: str = Field(default="doubao-embedding") + + # 智谱AI配置 + zhipu_api_key: Optional[str] = Field(default=None) + zhipu_base_url: str = Field(default="https://open.bigmodel.cn/api/paas/v4") + zhipu_embedding_model: str = Field(default="embedding-3") + + # 月之暗面配置 + moonshot_api_key: Optional[str] = Field(default=None) + moonshot_base_url: str = Field(default="https://api.moonshot.cn/v1") + moonshot_embedding_model: str = Field(default="moonshot-embedding") + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + + def get_current_config(self) -> dict: + """获取当前选择的embedding提供商配置 - 优先从数据库读取默认配置.""" + try: + # 尝试从数据库读取默认嵌入模型配置 + from th_agenter.services.llm_config_service import LLMConfigService + llm_service = LLMConfigService() + db_config = llm_service.get_default_embedding_config() + + if db_config: + # 如果数据库中有默认配置,使用数据库配置 + config = { + "api_key": db_config.api_key, + "base_url": db_config.base_url, + "model": db_config.model_name + } + return config + except Exception as e: + # 如果数据库读取失败,记录错误并回退到环境变量 + import logging + logging.warning(f"Failed to read embedding config from database, falling back to env vars: {e}") + + # 回退到原有的环境变量配置 + provider_configs = { + "openai": { + "api_key": self.openai_api_key, + "base_url": self.openai_base_url, + "model": self.openai_embedding_model + }, + "deepseek": { + "api_key": self.deepseek_api_key, + "base_url": self.deepseek_base_url, + "model": self.deepseek_embedding_model + }, + "doubao": { + "api_key": self.doubao_api_key, + "base_url": self.doubao_base_url, + "model": self.doubao_embedding_model + }, + "zhipu": { + "api_key": self.zhipu_api_key, + "base_url": self.zhipu_base_url, + "model": self.zhipu_embedding_model + }, + "moonshot": { + "api_key": self.moonshot_api_key, + "base_url": self.moonshot_base_url, + "model": self.moonshot_embedding_model + } + } + + return provider_configs.get(self.provider, provider_configs["zhipu"]) + + +class VectorDBSettings(BaseSettings): + """Vector database configuration.""" + type: str = Field(default="pgvector", alias="vector_db_type") + persist_directory: str = Field(default="./data/chroma") + collection_name: str = Field(default="documents") + embedding_dimension: int = Field(default=2048) # 智谱AI embedding-3模型的维度 + + # PostgreSQL pgvector configuration + pgvector_host: str = Field(default="localhost") + pgvector_port: int = Field(default=5432) + pgvector_database: str = Field(default="vectordb") + pgvector_user: str = Field(default="postgres") + pgvector_password: str = Field(default="") + pgvector_table_name: str = Field(default="embeddings") + pgvector_vector_dimension: int = Field(default=1024) + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + + +class FileSettings(BaseSettings): + """File processing configuration.""" + upload_dir: str = Field(default="./data/uploads") + max_size: int = Field(default=10485760) # 10MB + allowed_extensions: Union[str, List[str]] = Field(default=[".txt", ".pdf", ".docx", ".md"]) + chunk_size: int = Field(default=1000) + chunk_overlap: int = Field(default=200) + semantic_splitter_enabled: bool = Field(default=False) # 是否启用语义分割器 + + @field_validator('allowed_extensions', mode='before') + @classmethod + def parse_allowed_extensions(cls, v): + """Parse comma-separated string to list of extensions.""" + if isinstance(v, str): + # Split by comma and add dots if not present + extensions = [ext.strip() for ext in v.split(',')] + return [ext if ext.startswith('.') else f'.{ext}' for ext in extensions] + elif isinstance(v, list): + # Ensure all extensions start with dot + return [ext if ext.startswith('.') else f'.{ext}' for ext in v] + return v + + def get_allowed_extensions_list(self) -> List[str]: + """Get allowed extensions as a list.""" + if isinstance(self.allowed_extensions, list): + return self.allowed_extensions + elif isinstance(self.allowed_extensions, str): + # Split by comma and add dots if not present + extensions = [ext.strip() for ext in self.allowed_extensions.split(',')] + return [ext if ext.startswith('.') else f'.{ext}' for ext in extensions] + return [] + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + + +class StorageSettings(BaseSettings): + """Storage configuration.""" + storage_type: str = Field(default="local") # local or s3 + upload_directory: str = Field(default="./data/uploads") + + # S3 settings + s3_bucket_name: str = Field(default="chat-agent-files") + aws_access_key_id: Optional[str] = Field(default=None) + aws_secret_access_key: Optional[str] = Field(default=None) + aws_region: str = Field(default="us-east-1") + s3_endpoint_url: Optional[str] = Field(default=None) # For S3-compatible services + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + + +class LoggingSettings(BaseSettings): + """Logging configuration.""" + level: str = Field(default="INFO") + file: str = Field(default="./data/logs/app.log") + format: str = Field(default="%(asctime)s - %(name)s - %(levelname)s - %(message)s") + max_bytes: int = Field(default=10485760) # 10MB + backup_count: int = Field(default=5) + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + + +class CORSSettings(BaseSettings): + """CORS configuration.""" + allowed_origins: List[str] = Field(default=["*"]) + allowed_methods: List[str] = Field(default=["GET", "POST", "PUT", "DELETE", "OPTIONS"]) + allowed_headers: List[str] = Field(default=["*"]) + + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + + +class ChatSettings(BaseSettings): + """Chat configuration.""" + max_history_length: int = Field(default=10) + system_prompt: str = Field(default="你是一个有用的AI助手,请根据提供的上下文信息回答用户的问题。") + max_response_tokens: int = Field(default=1000) + + +class Settings(BaseSettings): + """Main application settings.""" + + # App info + app_name: str = Field(default="TH-Agenter") + app_version: str = Field(default="0.1.0") + debug: bool = Field(default=True) + environment: str = Field(default="development") + + # Server + host: str = Field(default="0.0.0.0") + port: int = Field(default=8000) + + # Configuration sections + database: DatabaseSettings = Field(default_factory=DatabaseSettings) + security: SecuritySettings = Field(default_factory=SecuritySettings) + llm: LLMSettings = Field(default_factory=LLMSettings) + embedding: EmbeddingSettings = Field(default_factory=EmbeddingSettings) + vector_db: VectorDBSettings = Field(default_factory=VectorDBSettings) + file: FileSettings = Field(default_factory=FileSettings) + storage: StorageSettings = Field(default_factory=StorageSettings) + logging: LoggingSettings = Field(default_factory=LoggingSettings) + cors: CORSSettings = Field(default_factory=CORSSettings) + chat: ChatSettings = Field(default_factory=ChatSettings) + tool: ToolSetings = Field(default_factory=ToolSetings) + model_config = { + "env_file": ".env", + "env_file_encoding": "utf-8", + "case_sensitive": False, + "extra": "ignore" + } + + @classmethod + def load_from_yaml(cls, config_path: str = "../configs/settings.yaml") -> "Settings": + """Load settings from YAML file.""" + config_file = Path(config_path) + + # 如果配置文件不存在,尝试从backend目录查找 + if not config_file.exists(): + # 获取当前文件所在目录(backend/th_agenter/core) + current_dir = Path(__file__).parent + # 向上两级到backend目录,然后找configs/settings.yaml + backend_config_path = current_dir.parent.parent / "configs" / "settings.yaml" + if backend_config_path.exists(): + config_file = backend_config_path + else: + return cls() + + with open(config_file, "r", encoding="utf-8") as f: + config_data = yaml.safe_load(f) or {} + + # 处理环境变量替换 + config_data = cls._resolve_env_vars_nested(config_data) + + # 为每个子设置类创建实例,确保它们能正确加载环境变量 + # 如果YAML中没有对应配置,则使用默认的BaseSettings加载(会自动读取.env文件) + settings_kwargs = {} + + # 显式处理各个子设置,以解决debug等情况因为环境的变化没有自动加载.env配置的问题 + settings_kwargs['database'] = DatabaseSettings(**(config_data.get('database', {}))) + settings_kwargs['security'] = SecuritySettings(**(config_data.get('security', {}))) + settings_kwargs['llm'] = LLMSettings(**(config_data.get('llm', {}))) + settings_kwargs['embedding'] = EmbeddingSettings(**(config_data.get('embedding', {}))) + settings_kwargs['vector_db'] = VectorDBSettings(**(config_data.get('vector_db', {}))) + settings_kwargs['file'] = FileSettings(**(config_data.get('file', {}))) + settings_kwargs['storage'] = StorageSettings(**(config_data.get('storage', {}))) + settings_kwargs['logging'] = LoggingSettings(**(config_data.get('logging', {}))) + settings_kwargs['cors'] = CORSSettings(**(config_data.get('cors', {}))) + settings_kwargs['chat'] = ChatSettings(**(config_data.get('chat', {}))) + settings_kwargs['tool'] = ToolSetings(**(config_data.get('tool', {}))) + + # 添加顶级配置 + for key, value in config_data.items(): + if key not in settings_kwargs: + settings_kwargs[key] = value + + return cls(**settings_kwargs) + + @staticmethod + def _flatten_config(config: Dict[str, Any], prefix: str = "") -> Dict[str, Any]: + """Flatten nested configuration dictionary.""" + flat = {} + for key, value in config.items(): + new_key = f"{prefix}_{key}" if prefix else key + if isinstance(value, dict): + flat.update(Settings._flatten_config(value, new_key)) + else: + flat[new_key] = value + return flat + + @staticmethod + def _resolve_env_vars_nested(config: Dict[str, Any]) -> Dict[str, Any]: + """Resolve environment variables in nested configuration.""" + if isinstance(config, dict): + return {key: Settings._resolve_env_vars_nested(value) for key, value in config.items()} + elif isinstance(config, str) and config.startswith("${") and config.endswith("}"): + env_var = config[2:-1] + return os.getenv(env_var, config) + else: + return config + + @staticmethod + def _resolve_env_vars(config: Dict[str, Any]) -> Dict[str, Any]: + """Resolve environment variables in configuration values.""" + resolved = {} + for key, value in config.items(): + if isinstance(value, str) and value.startswith("${") and value.endswith("}"): + env_var = value[2:-1] + resolved[key] = os.getenv(env_var, value) + else: + resolved[key] = value + return resolved + + +@lru_cache() +def get_settings() -> Settings: + """Get cached settings instance.""" + return Settings.load_from_yaml() + + +# Global settings instance +settings = get_settings() \ No newline at end of file diff --git a/backend/th_agenter/core/context.py b/backend/th_agenter/core/context.py new file mode 100644 index 0000000..debd894 --- /dev/null +++ b/backend/th_agenter/core/context.py @@ -0,0 +1,120 @@ +""" +HTTP请求上下文管理,如:获取当前登录用户信息及Token信息 +""" + +from contextvars import ContextVar +from typing import Optional +import threading +from ..models.user import User + +# Context variable to store current user +current_user_context: ContextVar[Optional[User]] = ContextVar('current_user', default=None) + +# Thread-local storage as backup +_thread_local = threading.local() + + +class UserContext: + """User context manager for accessing current user globally.""" + + @staticmethod + def set_current_user(user: User) -> None: + """Set current user in context.""" + import logging + logging.info(f"Setting user in context: {user.username} (ID: {user.id})") + + # Set in ContextVar + current_user_context.set(user) + + # Also set in thread-local as backup + _thread_local.current_user = user + + # Verify it was set + verify_user = current_user_context.get() + logging.info(f"Verification - ContextVar user: {verify_user.username if verify_user else None}") + + @staticmethod + def set_current_user_with_token(user: User): + """Set current user in context and return token for cleanup.""" + import logging + logging.info(f"Setting user in context with token: {user.username} (ID: {user.id})") + + # Set in ContextVar and get token + token = current_user_context.set(user) + + # Also set in thread-local as backup + _thread_local.current_user = user + + # Verify it was set + verify_user = current_user_context.get() + logging.info(f"Verification - ContextVar user: {verify_user.username if verify_user else None}") + + return token + + @staticmethod + def reset_current_user_token(token): + """Reset current user context using token.""" + import logging + logging.info("Resetting user context using token") + + # Reset ContextVar using token + current_user_context.reset(token) + + # Clear thread-local as well + if hasattr(_thread_local, 'current_user'): + delattr(_thread_local, 'current_user') + + @staticmethod + def get_current_user() -> Optional[User]: + """Get current user from context.""" + import logging + + # Try ContextVar first + user = current_user_context.get() + if user: + logging.debug(f"Got user from ContextVar: {user.username} (ID: {user.id})") + return user + + # Fallback to thread-local + user = getattr(_thread_local, 'current_user', None) + if user: + logging.debug(f"Got user from thread-local: {user.username} (ID: {user.id})") + return user + + logging.debug("No user found in context (neither ContextVar nor thread-local)") + return None + + @staticmethod + def get_current_user_id() -> Optional[int]: + """Get current user ID from context.""" + user = UserContext.get_current_user() + return user.id if user else None + + @staticmethod + def clear_current_user() -> None: + """Clear current user from context.""" + import logging + logging.info("Clearing user context") + + current_user_context.set(None) + if hasattr(_thread_local, 'current_user'): + delattr(_thread_local, 'current_user') + + @staticmethod + def require_current_user() -> User: + """Get current user from context, raise exception if not found.""" + # Use the same logic as get_current_user to check both ContextVar and thread-local + user = UserContext.get_current_user() + if user is None: + from fastapi import HTTPException, status + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="No authenticated user in context" + ) + return user + + @staticmethod + def require_current_user_id() -> int: + """Get current user ID from context, raise exception if not found.""" + user = UserContext.require_current_user() + return user.id \ No newline at end of file diff --git a/backend/th_agenter/core/exceptions.py b/backend/th_agenter/core/exceptions.py new file mode 100644 index 0000000..4791128 --- /dev/null +++ b/backend/th_agenter/core/exceptions.py @@ -0,0 +1,52 @@ +"""Custom exceptions for the application.""" + +from typing import Any, Dict, Optional + + +class BaseCustomException(Exception): + """Base custom exception class.""" + + def __init__(self, message: str, details: Optional[Dict[str, Any]] = None): + self.message = message + self.details = details or {} + super().__init__(self.message) + + +class NotFoundError(BaseCustomException): + """Exception raised when a resource is not found.""" + pass + + +class ValidationError(BaseCustomException): + """Exception raised when validation fails.""" + pass + + +class AuthenticationError(BaseCustomException): + """Exception raised when authentication fails.""" + pass + + +class AuthorizationError(BaseCustomException): + """Exception raised when authorization fails.""" + pass + + +class DatabaseError(BaseCustomException): + """Exception raised when database operations fail.""" + pass + + +class ConfigurationError(BaseCustomException): + """Exception raised when configuration is invalid.""" + pass + + +class ExternalServiceError(BaseCustomException): + """Exception raised when external service calls fail.""" + pass + + +class BusinessLogicError(BaseCustomException): + """Exception raised when business logic validation fails.""" + pass \ No newline at end of file diff --git a/backend/th_agenter/core/llm.py b/backend/th_agenter/core/llm.py new file mode 100644 index 0000000..c9d4b30 --- /dev/null +++ b/backend/th_agenter/core/llm.py @@ -0,0 +1,47 @@ +"""LLM工厂类,用于创建和管理LLM实例""" + +from typing import Optional +from langchain_openai import ChatOpenAI +from .config import get_settings + +def create_llm(model: Optional[str] = None, temperature: Optional[float] = None, streaming: bool = False) -> ChatOpenAI: + """创建LLM实例 + + Args: + model: 可选,指定使用的模型名称。如果不指定,将使用配置文件中的默认模型 + temperature: 可选,模型温度参数 + streaming: 是否启用流式响应,默认False + + Returns: + ChatOpenAI实例 + """ + settings = get_settings() + llm_config = settings.llm.get_current_config() + + if model: + # 根据指定的模型获取对应配置 + if model.startswith('deepseek'): + llm_config['model'] = settings.llm.deepseek_model + llm_config['api_key'] = settings.llm.deepseek_api_key + llm_config['base_url'] = settings.llm.deepseek_base_url + elif model.startswith('doubao'): + llm_config['model'] = settings.llm.doubao_model + llm_config['api_key'] = settings.llm.doubao_api_key + llm_config['base_url'] = settings.llm.doubao_base_url + elif model.startswith('glm'): + llm_config['model'] = settings.llm.zhipu_model + llm_config['api_key'] = settings.llm.zhipu_api_key + llm_config['base_url'] = settings.llm.zhipu_base_url + elif model.startswith('moonshot'): + llm_config['model'] = settings.llm.moonshot_model + llm_config['api_key'] = settings.llm.moonshot_api_key + llm_config['base_url'] = settings.llm.moonshot_base_url + + return ChatOpenAI( + model=llm_config['model'], + api_key=llm_config['api_key'], + base_url=llm_config['base_url'], + temperature=temperature if temperature is not None else llm_config['temperature'], + max_tokens=llm_config['max_tokens'], + streaming=streaming + ) \ No newline at end of file diff --git a/backend/th_agenter/core/logging.py b/backend/th_agenter/core/logging.py new file mode 100644 index 0000000..2a0e333 --- /dev/null +++ b/backend/th_agenter/core/logging.py @@ -0,0 +1,64 @@ +"""Logging configuration for TH-Agenter.""" + +import logging +import logging.handlers +from pathlib import Path +from typing import Optional + +from .config import LoggingSettings + + +def setup_logging(logging_config: LoggingSettings) -> None: + """Setup application logging.""" + + # 确保使用绝对路径,避免在不同目录运行时路径不一致 + log_file_path = logging_config.file + if not Path(log_file_path).is_absolute(): + # 如果是相对路径,则基于项目根目录计算绝对路径 + # 项目根目录是backend的父目录 + backend_dir = Path(__file__).parent.parent.parent + log_file_path = str(backend_dir / log_file_path) + + # Create logs directory if it doesn't exist + log_file = Path(log_file_path) + log_file.parent.mkdir(parents=True, exist_ok=True) + + # Configure root logger + root_logger = logging.getLogger() + root_logger.setLevel(getattr(logging, logging_config.level.upper())) + + # Clear existing handlers + root_logger.handlers.clear() + + # Create formatter + formatter = logging.Formatter(logging_config.format) + + # Console handler + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_handler.setFormatter(formatter) + root_logger.addHandler(console_handler) + + # File handler with rotation + file_handler = logging.handlers.RotatingFileHandler( + filename=log_file_path, + maxBytes=logging_config.max_bytes, + backupCount=logging_config.backup_count, + encoding="utf-8" + ) + file_handler.setLevel(getattr(logging, logging_config.level.upper())) + file_handler.setFormatter(formatter) + root_logger.addHandler(file_handler) + + # Set specific logger levels + logging.getLogger("uvicorn").setLevel(logging.INFO) + logging.getLogger("uvicorn.access").setLevel(logging.WARNING) + logging.getLogger("fastapi").setLevel(logging.INFO) + logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING) + + logging.info("Logging configured successfully") + + +def get_logger(name: Optional[str] = None) -> logging.Logger: + """Get a logger instance.""" + return logging.getLogger(name or __name__) \ No newline at end of file diff --git a/backend/th_agenter/core/middleware.py b/backend/th_agenter/core/middleware.py new file mode 100644 index 0000000..56d5bf8 --- /dev/null +++ b/backend/th_agenter/core/middleware.py @@ -0,0 +1,163 @@ +""" +中间件管理,如上下文中间件:校验Token等 +""" + +from fastapi import Request, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from sqlalchemy.orm import Session +from starlette.middleware.base import BaseHTTPMiddleware +from starlette.responses import Response +from typing import Callable + +from ..db.database import get_db_session +from ..services.auth import AuthService +from .context import UserContext + + +class UserContextMiddleware(BaseHTTPMiddleware): + """Middleware to set user context for authenticated requests.""" + + def __init__(self, app, exclude_paths: list = None): + super().__init__(app) + # Paths that don't require authentication + self.exclude_paths = exclude_paths or [ + "/docs", + "/redoc", + "/openapi.json", + "/api/auth/login", + "/api/auth/register", + "/api/auth/login-oauth", + "/auth/login", + "/auth/register", + "/auth/login-oauth", + "/health", + "/test" + ] + + async def dispatch(self, request: Request, call_next: Callable) -> Response: + """Process request and set user context if authenticated.""" + import logging + logging.info(f"[MIDDLEWARE] Processing request: {request.method} {request.url.path}") + + # Skip authentication for excluded paths + path = request.url.path + logging.info(f"[MIDDLEWARE] Checking path: {path} against exclude_paths: {self.exclude_paths}") + + should_skip = False + for exclude_path in self.exclude_paths: + # Exact match + if path == exclude_path: + should_skip = True + logging.info(f"[MIDDLEWARE] Path {path} exactly matches exclude_path {exclude_path}") + break + # For paths ending with '/', check if request path starts with it + elif exclude_path.endswith('/') and path.startswith(exclude_path): + should_skip = True + logging.info(f"[MIDDLEWARE] Path {path} starts with exclude_path {exclude_path}") + break + # For paths not ending with '/', check if request path starts with it + '/' + elif not exclude_path.endswith('/') and exclude_path != '/' and path.startswith(exclude_path + '/'): + should_skip = True + logging.info(f"[MIDDLEWARE] Path {path} starts with exclude_path {exclude_path}/") + break + + if should_skip: + logging.info(f"[MIDDLEWARE] Skipping authentication for excluded path: {path}") + response = await call_next(request) + return response + + logging.info(f"[MIDDLEWARE] Processing authenticated request: {path}") + + # Always clear any existing user context to ensure fresh authentication + UserContext.clear_current_user() + + # Initialize context token + user_token = None + + # Try to extract and validate token + try: + # Get authorization header + authorization = request.headers.get("Authorization") + if not authorization or not authorization.startswith("Bearer "): + # No token provided, return 401 error + from fastapi import HTTPException, status + from fastapi.responses import JSONResponse + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content={"detail": "Missing or invalid authorization header"}, + headers={"WWW-Authenticate": "Bearer"} + ) + + # Extract token + token = authorization.split(" ")[1] + + # Verify token + payload = AuthService.verify_token(token) + if payload is None: + # Invalid token, return 401 error + from fastapi import HTTPException, status + from fastapi.responses import JSONResponse + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content={"detail": "Invalid or expired token"}, + headers={"WWW-Authenticate": "Bearer"} + ) + + # Get username from token + username = payload.get("sub") + if not username: + from fastapi import HTTPException, status + from fastapi.responses import JSONResponse + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content={"detail": "Invalid token payload"}, + headers={"WWW-Authenticate": "Bearer"} + ) + + # Get user from database + db = get_db_session() + try: + from ..models.user import User + user = db.query(User).filter(User.username == username).first() + if not user: + from fastapi import HTTPException, status + from fastapi.responses import JSONResponse + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content={"detail": "User not found"}, + headers={"WWW-Authenticate": "Bearer"} + ) + + if not user.is_active: + from fastapi import HTTPException, status + from fastapi.responses import JSONResponse + return JSONResponse( + status_code=status.HTTP_401_UNAUTHORIZED, + content={"detail": "User account is inactive"}, + headers={"WWW-Authenticate": "Bearer"} + ) + + # Set user in context using token mechanism + user_token = UserContext.set_current_user_with_token(user) + import logging + logging.info(f"User {user.username} (ID: {user.id}) authenticated and set in context") + + # Verify context is set correctly + current_user_id = UserContext.get_current_user_id() + logging.info(f"Verified current user ID in context: {current_user_id}") + finally: + db.close() + + except Exception as e: + # Log error but don't fail the request + import logging + logging.warning(f"Error setting user context: {e}") + + # Continue with request + try: + response = await call_next(request) + return response + finally: + # Always clear user context after request processing + UserContext.clear_current_user() + logging.debug(f"[MIDDLEWARE] Cleared user context after processing request: {path}") \ No newline at end of file diff --git a/backend/th_agenter/core/simple_permissions.py b/backend/th_agenter/core/simple_permissions.py new file mode 100644 index 0000000..30ea5e4 --- /dev/null +++ b/backend/th_agenter/core/simple_permissions.py @@ -0,0 +1,90 @@ +"""简化的权限检查系统.""" + +from functools import wraps +from typing import Optional +from fastapi import HTTPException, Depends +from sqlalchemy.orm import Session + +from ..db.database import get_db +from ..models.user import User +from ..models.permission import Role +from ..services.auth import AuthService + + +def is_super_admin(user: User, db: Session) -> bool: + """检查用户是否为超级管理员.""" + if not user or not user.is_active: + return False + + # 检查用户是否有超级管理员角色 + for role in user.roles: + if role.code == "SUPER_ADMIN": + return True + return False + + +def require_super_admin( + current_user: User = Depends(AuthService.get_current_user), + db: Session = Depends(get_db) +) -> User: + """要求超级管理员权限的依赖项.""" + if not is_super_admin(current_user, db): + raise HTTPException( + status_code=403, + detail="需要超级管理员权限" + ) + return current_user + + +def require_authenticated_user( + current_user: User = Depends(AuthService.get_current_user) +) -> User: + """要求已认证用户的依赖项.""" + if not current_user or not current_user.is_active: + raise HTTPException( + status_code=401, + detail="需要登录" + ) + return current_user + + +class SimplePermissionChecker: + """简化的权限检查器.""" + + def __init__(self, db: Session): + self.db = db + + def check_super_admin(self, user: User) -> bool: + """检查是否为超级管理员.""" + return is_super_admin(user, self.db) + + def check_user_access(self, user: User, target_user_id: int) -> bool: + """检查用户访问权限(自己或超级管理员).""" + if not user or not user.is_active: + return False + + # 超级管理员可以访问所有用户 + if self.check_super_admin(user): + return True + + # 用户只能访问自己的信息 + return user.id == target_user_id + + +# 权限装饰器 +def super_admin_required(func): + """超级管理员权限装饰器.""" + @wraps(func) + def wrapper(*args, **kwargs): + # 这个装饰器主要用于服务层,实际的FastAPI依赖项检查在路由层 + return func(*args, **kwargs) + return wrapper + + +def authenticated_required(func): + """认证用户权限装饰器.""" + @wraps(func) + def wrapper(*args, **kwargs): + # 这个装饰器主要用于服务层,实际的FastAPI依赖项检查在路由层 + return func(*args, **kwargs) + return wrapper \ No newline at end of file diff --git a/backend/th_agenter/core/user_utils.py b/backend/th_agenter/core/user_utils.py new file mode 100644 index 0000000..fff0e81 --- /dev/null +++ b/backend/th_agenter/core/user_utils.py @@ -0,0 +1,76 @@ +"""User utility functions for easy access to current user context.""" + +from typing import Optional +from ..models.user import User +from .context import UserContext + + +def get_current_user() -> Optional[User]: + """Get current authenticated user from context. + + Returns: + Current user if authenticated, None otherwise + """ + return UserContext.get_current_user() + + +def get_current_user_id() -> Optional[int]: + """Get current authenticated user ID from context. + + Returns: + Current user ID if authenticated, None otherwise + """ + return UserContext.get_current_user_id() + + +def require_current_user() -> User: + """Get current authenticated user from context, raise exception if not found. + + Returns: + Current user + + Raises: + HTTPException: If no authenticated user in context + """ + return UserContext.require_current_user() + + +def require_current_user_id() -> int: + """Get current authenticated user ID from context, raise exception if not found. + + Returns: + Current user ID + + Raises: + HTTPException: If no authenticated user in context + """ + return UserContext.require_current_user_id() + + +def is_user_authenticated() -> bool: + """Check if there is an authenticated user in the current context. + + Returns: + True if user is authenticated, False otherwise + """ + return UserContext.get_current_user() is not None + + +def get_current_username() -> Optional[str]: + """Get current authenticated user's username from context. + + Returns: + Current user's username if authenticated, None otherwise + """ + user = UserContext.get_current_user() + return user.username if user else None + + +def get_current_user_email() -> Optional[str]: + """Get current authenticated user's email from context. + + Returns: + Current user's email if authenticated, None otherwise + """ + user = UserContext.get_current_user() + return user.email if user else None \ No newline at end of file diff --git a/backend/th_agenter/db/__init__.py b/backend/th_agenter/db/__init__.py new file mode 100644 index 0000000..7ffefdc --- /dev/null +++ b/backend/th_agenter/db/__init__.py @@ -0,0 +1,6 @@ +"""Database module for TH-Agenter.""" + +from .database import get_db, init_db +from .base import Base + +__all__ = ["get_db", "init_db", "Base"] \ No newline at end of file diff --git a/backend/th_agenter/db/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/db/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..1bac90e7c57147c8ac593d3bc0e9ff44f27b81a0 GIT binary patch literal 341 zcmey&%ge<81acn?GJAmZV-N=hn4pZ$IzYx$hG2#whG52ECT~VBrXnUU<|1YV5TDtb z#f!CwRe?dDA($nQv4}05Rg8U00DM`23GxIV*3{A#cEKWdenn2@$DvDS@1S^p6(`3KJ9v`2QpBx{5 ziz5YUIzrJc7LaXVr66^QIXUt1D;Yimbu!$tbG3@eFG$S;Tdtd%n3)$-lA)UjaY{^5 zVsdtBUJ6hs9wwBM6cZm0c2T@uLFFwD8;B?Digfm~iJ3?x1=BoGBYP1yP#gdrG+G7# literal 0 HcmV?d00001 diff --git a/backend/th_agenter/db/__pycache__/base.cpython-313.pyc b/backend/th_agenter/db/__pycache__/base.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ff90775fc7fa23a18fab68e46e65a14f93b3e4cf GIT binary patch literal 2752 zcma)8O>7)R7JmIR-97D@cpRJ9OxAW|lHf)#F$>AAy&+4$vB-Eygxg*L8ECb;YbH&4 zx=nRYVw;OktBrQi=0H%8kbn~b4sb+pSnX|(>qM(0B}If^DYwa*h`@!`;ix4%M z*RQHxzk2n)uU_>=KCc6|p6PE+Jg)(K#XFlV(^fVTRL+3~6fD6KXN9sr1%vC-tXP() zRFJhqhhQRoD2JGS(pwmTa?qVf`E zq5@RFY>1XXB}=5TCDBYp8H0*yN%)f|C90+UY(<~jai^=6-rc*Eu@tn<{W|Mk;Z2>C zX}%MSHYw6=TY9|<@R(p_i}12W3s$eCSNba3InY*5rT;P6%HfRsvOou{d=cn9);79h z0Uj6V;4Ux=Ec+zp>lu#0jXb`yI>P%#3W{;sPfi$aRIf*&QT0jS#ie;uWIapNuRCYer0x#1Ctwqwj+$u}s`p;*_&(AD!4XE8x7HzF@a^MD~bS3!p57k^v*IApKIlLjm7GJZr*& zIF3Ge^N>!@6$0)%ZK!t;s|j0s_rY;x(@q5kV=-V9y;b3HX%wo0DX)HRkz^}g)rcA{ zB}l8itvwl-X}j)3)} zH$=<;7Ob8H1na5RYNo<+E>^lydv!X7HmtvfOmrKRB3bMXw1<~nl7`LnR#rTFafRuv zFt~2Yl+%WeaKX8gdW$(gJWF&ZA41@P6}yDHoAkptA3uv?4VneW^`H6UwcKx;qMRSR zQrvyHIJsV&yj1+dS+$uc z`{C*P0XSRdI=t)}AK$zc;JKfj-`DnSi_h%l<;p;hp|A^OORFM$tG_|$P7#{4OK1`q zp6{kN5M3$ZrG`ZUZ@++GMN!@Kev6qLI(Vn42&hqEL|>6Tr5s(WTb-rWpU!H(v%?I|Bu--vu_Q)Gw|N4@4kBJ z-bXLy9=%$exXEprAZ7I*{s*YW{H(tD;2}lEaQ2{1A3$M>M`z3-UKDx6xCPy|@v3N~ z+sWDXD-9=T&-B_@CTW}men`TI@7A`x$VCP1oW(eyj*DfLavUgMjETNSc@>~p-jl-U zbeG9a(q*!fbOTBYB<0^bO!C8o$%|1Gq$H+$`QSZRi!rIbYxB(di;aX}d1m=`rFKaW z*fw5BMKq52h4e)0801i;T<|+F{ws>xe}Sv=`1St5wLg84?LDi$Gyn0xdUpTXlUMr& z&ubTUeKh{T_=gXE(pc|1yjI>imi%-1)8)@r|1F>dO` z%$^I{$Nlf;n}F)S2X67l_rK3%5FQkipTIPGVA6%4|Zl=et1o)A2g49>KIc?>W z)0!2#^Chffr8_>EC_h7&-6`tiT_lI0klqp%3Oe?EMP((RqWmHaTC2=FgKP@F23Af6jR6(rB5yfb^ zOV2JNi)anSJ>TXXs$zPPS2mF4f&zHL2=aC*<QBGfq{;djgqyu zyKLmwuvPGeb3j#=yHr?S^7l?vHt4|)UHM}j-Xf!5qZg_Z0H`oPP#)mxYA{b%pW=UPgC zQ_&iV_F1B-q#8=>_HCb;FUs0y(VoYgT{G^A1)!WjUIM%OHR-Nc0=&asFIPLD~s2{(@+P_ zhG~++Y(Bx7acCSC&A@A*kmo_9QVD)GS6C)f&^yjFehSUQHrVCpdzbIY!(YqVKSUJF za!uivJ0K|UpMURsQ%E#~#OLF6_D$Xs!tXC!S*Z6;-W87i%d>K2c4#)iQITM`O037w z!g7vLsM|4ct7k>a(*pc3g!p&SbxuP}%0yt#J*A7bA~%Av45wa^kntlH_F)x!*gd7Y zD$5wqD;Y0+^LyxfN~&y~aO>R>1E6I-9TQA&{^2pTq7h^!z+4 zb)j}>7cqYBg&AkgXi14Me77N^oVk?86%Uv)u}zcFp9cdP^weoEZ`uw^$^wOD%3+7) zNIo7!mV5ymPQuvhP#qH*9}ayD>2Mnl50w6^!<%zk0Vx=7MPpaxEfp!@Yx0NkC!v4t zgpu06jrd@+C5NtsKMa2yVXPMUC{jN(b*uLlUq3kgwS1x_M}F9nKL*))?fWwb7@&f;Qo7Sou|ATXxk+rjmi~#eadw7+7rkRkVGNkjHx7PhN$Y3{;0P zk{3zjM{YAbaP?GO81&XB<5bG!3U)r1Yrk3uQU=4!uj2AXJJd$I11X!7y^u?oPf-!# zI|N$7yqt#fHo++%s+Cf)9moYnN*-CX8D$9;p2e}|&?(b%_W>OM;R6^-9V2O;Q%qMKr?C%&nEFCO8_-1o0@D)-{n la1R&%RJrlOXVW+P>O;pGvD6NNp{+5A8@eI=9l^)5`yXz9-|7GW literal 0 HcmV?d00001 diff --git a/backend/th_agenter/db/__pycache__/init_system_data.cpython-313.pyc b/backend/th_agenter/db/__pycache__/init_system_data.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..30b5c940400c622f5031319d7e15fdec8a3c1cc9 GIT binary patch literal 4229 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 literal 0 HcmV?d00001 diff --git a/backend/th_agenter/db/base.py b/backend/th_agenter/db/base.py new file mode 100644 index 0000000..e631680 --- /dev/null +++ b/backend/th_agenter/db/base.py @@ -0,0 +1,62 @@ +"""Database base model.""" + +from datetime import datetime +from sqlalchemy import Column, Integer, DateTime, ForeignKey +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.sql import func +from typing import Optional + + +Base = declarative_base() + + +class BaseModel(Base): + """Base model with common fields.""" + + __abstract__ = True + + id = Column(Integer, primary_key=True, index=True) + created_at = Column(DateTime, default=func.now(), nullable=False) + updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False) + created_by = Column(Integer, nullable=True) + updated_by = Column(Integer, nullable=True) + + def __init__(self, **kwargs): + """Initialize model with automatic audit fields setting.""" + super().__init__(**kwargs) + # Set audit fields for new instances + self.set_audit_fields() + def set_audit_fields(self, user_id: Optional[int] = None, is_update: bool = False): + """Set audit fields for create/update operations. + + Args: + user_id: ID of the user performing the operation (optional, will use context if not provided) + is_update: True for update operations, False for create operations + """ + # Get user_id from context if not provided + if user_id is None: + from ..core.context import UserContext + try: + user_id = UserContext.get_current_user_id() + except Exception: + # If no user in context, skip setting audit fields + return + + # Skip if still no user_id + if user_id is None: + return + + if not is_update: + # For create operations, set both create_by and update_by + self.created_by = user_id + self.updated_by = user_id + else: + # For update operations, only set update_by + self.updated_by = user_id + + def to_dict(self): + """Convert model to dictionary.""" + return { + column.name: getattr(self, column.name) + for column in self.__table__.columns + } \ No newline at end of file diff --git a/backend/th_agenter/db/database.py b/backend/th_agenter/db/database.py new file mode 100644 index 0000000..d566cbf --- /dev/null +++ b/backend/th_agenter/db/database.py @@ -0,0 +1,89 @@ +"""Database connection and session management.""" + +import logging +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, Session +from typing import Generator + +from ..core.config import get_settings +from .base import Base + + +# Global variables +engine = None +SessionLocal = None + + +def create_database_engine(): + """Create database engine.""" + global engine, SessionLocal + + settings = get_settings() + database_url = settings.database.url + + # Determine database type and configure engine + engine_kwargs = { + "echo": settings.database.echo, + } + + if database_url.startswith("sqlite"): + # SQLite configuration + engine = create_engine(database_url, **engine_kwargs) + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + logging.info(f"SQLite database engine created: {database_url}") + elif database_url.startswith("postgresql"): + # PostgreSQL configuration + engine_kwargs.update({ + "pool_size": settings.database.pool_size, + "max_overflow": settings.database.max_overflow, + "pool_pre_ping": True, + "pool_recycle": 3600, + }) + engine = create_engine(database_url, **engine_kwargs) + SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + logging.info(f"PostgreSQL database engine created: {database_url}") + else: + raise ValueError(f"Unsupported database type. Please use PostgreSQL or SQLite. URL: {database_url}") + + +async def init_db(): + """Initialize database.""" + global engine + + if engine is None: + create_database_engine() + + # Import all models to ensure they are registered + from ..models import user, conversation, message, knowledge_base, permission, workflow + + # Create all tables + Base.metadata.create_all(bind=engine) + logging.info("Database tables created") + + +def get_db() -> Generator[Session, None, None]: + """Get database session.""" + global SessionLocal + + if SessionLocal is None: + create_database_engine() + + db = SessionLocal() + try: + yield db + except Exception as e: + db.rollback() + logging.error(f"Database session error: {e}") + raise + finally: + db.close() + + +def get_db_session() -> Session: + """Get database session (synchronous).""" + global SessionLocal + + if SessionLocal is None: + create_database_engine() + + return SessionLocal() \ No newline at end of file diff --git a/backend/th_agenter/db/db_config_key.key b/backend/th_agenter/db/db_config_key.key new file mode 100644 index 0000000..c71bfaf --- /dev/null +++ b/backend/th_agenter/db/db_config_key.key @@ -0,0 +1 @@ +nRWvsuXfWm1IXThkPQ7lA4HlTiNP4CkCYxqczEfrRR4= \ No newline at end of file diff --git a/backend/th_agenter/db/init_system_data.py b/backend/th_agenter/db/init_system_data.py new file mode 100644 index 0000000..ddef4da --- /dev/null +++ b/backend/th_agenter/db/init_system_data.py @@ -0,0 +1,121 @@ +"""Initialize system management data.""" + +from sqlalchemy.orm import Session +from ..models.permission import Role +from ..models.user import User +from ..services.auth import AuthService +from ..utils.logger import get_logger + +logger = get_logger(__name__) + + + + + +def init_roles(db: Session) -> None: + """初始化系统角色.""" + roles_data = [ + { + "name": "超级管理员", + "code": "SUPER_ADMIN", + "description": "系统超级管理员,拥有所有权限" + }, + { + "name": "普通用户", + "code": "USER", + "description": "普通用户,基础功能权限" + } + ] + + for role_data in roles_data: + # 检查角色是否已存在 + existing_role = db.query(Role).filter( + Role.code == role_data["code"] + ).first() + + if not existing_role: + # 创建角色 + role = Role( + name=role_data["name"], + code=role_data["code"], + description=role_data["description"] + ) + role.set_audit_fields(1) # 系统用户ID为1 + db.add(role) + logger.info(f"Created role: {role_data['name']} ({role_data['code']})") + + db.commit() + logger.info("Roles initialization completed") + + + + + +def init_admin_user(db: Session) -> None: + """初始化默认管理员用户.""" + logger.info("Starting admin user initialization...") + + # 检查是否已存在管理员用户 + existing_admin = db.query(User).filter( + User.username == "admin" + ).first() + + if existing_admin: + logger.info("Admin user already exists") + return + + # 创建管理员用户 + hashed_password = AuthService.get_password_hash("admin123") + + admin_user = User( + username="admin", + email="admin@example.com", + hashed_password=hashed_password, + full_name="系统管理员", + is_active=True + ) + + admin_user.set_audit_fields(1) + db.add(admin_user) + db.flush() + + # 分配超级管理员角色 + super_admin_role = db.query(Role).filter( + Role.code == "SUPER_ADMIN" + ).first() + + if super_admin_role: + admin_user.roles.append(super_admin_role) + + db.commit() + logger.info("Admin user created: admin / admin123") + + +def init_system_data(db: Session) -> None: + """初始化所有系统数据.""" + logger.info("Starting system data initialization...") + + try: + # 初始化角色 + init_roles(db) + + # 初始化管理员用户 + init_admin_user(db) + + logger.info("System data initialization completed successfully") + + except Exception as e: + logger.error(f"Error during system data initialization: {str(e)}") + db.rollback() + raise + + +if __name__ == "__main__": + # 可以单独运行此脚本来初始化数据 + from ..db.database import SessionLocal + + db = SessionLocal() + try: + init_system_data(db) + finally: + db.close() \ No newline at end of file diff --git a/backend/th_agenter/db/migrations/add_system_management.py b/backend/th_agenter/db/migrations/add_system_management.py new file mode 100644 index 0000000..8bbd9f0 --- /dev/null +++ b/backend/th_agenter/db/migrations/add_system_management.py @@ -0,0 +1,216 @@ +"""Add system management tables. + +Revision ID: add_system_management +Revises: +Create Date: 2024-01-01 00:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import mysql + +# revision identifiers, used by Alembic. +revision = 'add_system_management' +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade(): + """Create system management tables.""" + + # Create departments table + op.create_table('departments', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('code', sa.String(length=50), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('parent_id', sa.Integer(), nullable=True), + sa.Column('sort_order', sa.Integer(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['parent_id'], ['departments.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('code') + ) + op.create_index(op.f('ix_departments_name'), 'departments', ['name'], unique=False) + op.create_index(op.f('ix_departments_parent_id'), 'departments', ['parent_id'], unique=False) + + # Create permissions table + op.create_table('permissions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('code', sa.String(length=100), nullable=False), + sa.Column('category', sa.String(length=50), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('sort_order', sa.Integer(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('code') + ) + op.create_index(op.f('ix_permissions_category'), 'permissions', ['category'], unique=False) + op.create_index(op.f('ix_permissions_name'), 'permissions', ['name'], unique=False) + + # Create roles table + op.create_table('roles', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('code', sa.String(length=50), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('sort_order', sa.Integer(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('code') + ) + op.create_index(op.f('ix_roles_name'), 'roles', ['name'], unique=False) + + # Create role_permissions table + op.create_table('role_permissions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('role_id', sa.Integer(), nullable=False), + sa.Column('permission_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ), + sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('role_id', 'permission_id', name='uq_role_permission') + ) + op.create_index(op.f('ix_role_permissions_permission_id'), 'role_permissions', ['permission_id'], unique=False) + op.create_index(op.f('ix_role_permissions_role_id'), 'role_permissions', ['role_id'], unique=False) + + # Create user_roles table + op.create_table('user_roles', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('role_id', sa.Integer(), nullable=False), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id', 'role_id', name='uq_user_role') + ) + op.create_index(op.f('ix_user_roles_role_id'), 'user_roles', ['role_id'], unique=False) + op.create_index(op.f('ix_user_roles_user_id'), 'user_roles', ['user_id'], unique=False) + + # Create user_permissions table + op.create_table('user_permissions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('permission_id', sa.Integer(), nullable=False), + sa.Column('granted', sa.Boolean(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('user_id', 'permission_id', name='uq_user_permission') + ) + op.create_index(op.f('ix_user_permissions_permission_id'), 'user_permissions', ['permission_id'], unique=False) + op.create_index(op.f('ix_user_permissions_user_id'), 'user_permissions', ['user_id'], unique=False) + + # Create llm_configs table + op.create_table('llm_configs', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=100), nullable=False), + sa.Column('provider', sa.String(length=50), nullable=False), + sa.Column('model_name', sa.String(length=100), nullable=False), + sa.Column('api_key', sa.Text(), nullable=True), + sa.Column('api_base', sa.String(length=500), nullable=True), + sa.Column('api_version', sa.String(length=20), nullable=True), + sa.Column('max_tokens', sa.Integer(), nullable=True), + sa.Column('temperature', sa.Float(), nullable=True), + sa.Column('top_p', sa.Float(), nullable=True), + sa.Column('frequency_penalty', sa.Float(), nullable=True), + sa.Column('presence_penalty', sa.Float(), nullable=True), + sa.Column('timeout', sa.Integer(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=True), + sa.Column('is_default', sa.Boolean(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('sort_order', sa.Integer(), nullable=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.Column('created_by', sa.Integer(), nullable=True), + sa.Column('updated_by', sa.Integer(), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_llm_configs_name'), 'llm_configs', ['name'], unique=False) + op.create_index(op.f('ix_llm_configs_provider'), 'llm_configs', ['provider'], unique=False) + + # Add new columns to users table + op.add_column('users', sa.Column('department_id', sa.Integer(), nullable=True)) + op.add_column('users', sa.Column('is_superuser', sa.Boolean(), nullable=True, default=False)) + op.add_column('users', sa.Column('is_admin', sa.Boolean(), nullable=True, default=False)) + op.add_column('users', sa.Column('last_login_at', sa.DateTime(), nullable=True)) + op.add_column('users', sa.Column('login_count', sa.Integer(), nullable=True, default=0)) + + # Create foreign key constraint for department_id + op.create_foreign_key('fk_users_department_id', 'users', 'departments', ['department_id'], ['id']) + op.create_index(op.f('ix_users_department_id'), 'users', ['department_id'], unique=False) + + +def downgrade(): + """Drop system management tables.""" + + # Drop foreign key and index for users.department_id + op.drop_index(op.f('ix_users_department_id'), table_name='users') + op.drop_constraint('fk_users_department_id', 'users', type_='foreignkey') + + # Drop new columns from users table + op.drop_column('users', 'login_count') + op.drop_column('users', 'last_login_at') + op.drop_column('users', 'is_admin') + op.drop_column('users', 'is_superuser') + op.drop_column('users', 'department_id') + + # Drop llm_configs table + op.drop_index(op.f('ix_llm_configs_provider'), table_name='llm_configs') + op.drop_index(op.f('ix_llm_configs_name'), table_name='llm_configs') + op.drop_table('llm_configs') + + # Drop user_permissions table + op.drop_index(op.f('ix_user_permissions_user_id'), table_name='user_permissions') + op.drop_index(op.f('ix_user_permissions_permission_id'), table_name='user_permissions') + op.drop_table('user_permissions') + + # Drop user_roles table + op.drop_index(op.f('ix_user_roles_user_id'), table_name='user_roles') + op.drop_index(op.f('ix_user_roles_role_id'), table_name='user_roles') + op.drop_table('user_roles') + + # Drop role_permissions table + op.drop_index(op.f('ix_role_permissions_role_id'), table_name='role_permissions') + op.drop_index(op.f('ix_role_permissions_permission_id'), table_name='role_permissions') + op.drop_table('role_permissions') + + # Drop roles table + op.drop_index(op.f('ix_roles_name'), table_name='roles') + op.drop_table('roles') + + # Drop permissions table + op.drop_index(op.f('ix_permissions_name'), table_name='permissions') + op.drop_index(op.f('ix_permissions_category'), table_name='permissions') + op.drop_table('permissions') + + # Drop departments table + op.drop_index(op.f('ix_departments_parent_id'), table_name='departments') + op.drop_index(op.f('ix_departments_name'), table_name='departments') + op.drop_table('departments') \ No newline at end of file diff --git a/backend/th_agenter/db/migrations/add_user_department_table.py b/backend/th_agenter/db/migrations/add_user_department_table.py new file mode 100644 index 0000000..edfa137 --- /dev/null +++ b/backend/th_agenter/db/migrations/add_user_department_table.py @@ -0,0 +1,83 @@ +"""Add user_department association table migration.""" + +import sys +import os +from pathlib import Path + +# 添加项目根目录到Python路径 +project_root = Path(__file__).parent.parent.parent.parent +sys.path.insert(0, str(project_root)) + +import asyncio +import asyncpg +from th_agenter.core.config import get_settings + +async def create_user_department_table(): + """Create user_departments association table.""" + settings = get_settings() + database_url = settings.database.url + + print(f"Database URL: {database_url}") + + try: + # 解析PostgreSQL连接URL + # postgresql://user:password@host:port/database + url_parts = database_url.replace('postgresql://', '').split('/') + db_name = url_parts[1] if len(url_parts) > 1 else 'postgres' + user_host = url_parts[0].split('@') + user_pass = user_host[0].split(':') + host_port = user_host[1].split(':') + + user = user_pass[0] + password = user_pass[1] if len(user_pass) > 1 else '' + host = host_port[0] + port = int(host_port[1]) if len(host_port) > 1 else 5432 + + # 连接PostgreSQL数据库 + conn = await asyncpg.connect( + user=user, + password=password, + database=db_name, + host=host, + port=port + ) + + # 创建user_departments表 + create_table_sql = """ + CREATE TABLE IF NOT EXISTS user_departments ( + id SERIAL PRIMARY KEY, + user_id INTEGER NOT NULL, + department_id INTEGER NOT NULL, + is_primary BOOLEAN NOT NULL DEFAULT true, + is_active BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (department_id) REFERENCES departments (id) ON DELETE CASCADE + ); + """ + + await conn.execute(create_table_sql) + + # 创建索引 + create_indexes_sql = [ + "CREATE INDEX IF NOT EXISTS idx_user_departments_user_id ON user_departments (user_id);", + "CREATE INDEX IF NOT EXISTS idx_user_departments_department_id ON user_departments (department_id);", + "CREATE UNIQUE INDEX IF NOT EXISTS idx_user_departments_unique ON user_departments (user_id, department_id);" + ] + + for index_sql in create_indexes_sql: + await conn.execute(index_sql) + + print("User departments table created successfully") + + except Exception as e: + print(f"Error creating user departments table: {e}") + raise + finally: + if 'conn' in locals(): + await conn.close() + + +if __name__ == "__main__": + asyncio.run(create_user_department_table()) \ No newline at end of file diff --git a/backend/th_agenter/db/migrations/migrate_hardcoded_resources.py b/backend/th_agenter/db/migrations/migrate_hardcoded_resources.py new file mode 100644 index 0000000..fb3474c --- /dev/null +++ b/backend/th_agenter/db/migrations/migrate_hardcoded_resources.py @@ -0,0 +1,451 @@ +#!/usr/bin/env python3 +"""Migration script to move hardcoded resources to database.""" + +import sys +import os +from pathlib import Path + +# Add the backend directory to Python path +backend_dir = Path(__file__).parent.parent.parent +sys.path.insert(0, str(backend_dir)) + +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker +from th_agenter.core.config import settings +from th_agenter.db.database import Base, get_db_session +from th_agenter.models import * # Import all models to ensure they're registered +from th_agenter.utils.logger import get_logger +from th_agenter.models.resource import Resource +from th_agenter.models.permission import Role +from th_agenter.models.resource import RoleResource + +logger = get_logger(__name__) + +def migrate_hardcoded_resources(): + """Migrate hardcoded resources from init_resource_data.py to database.""" + db = None + try: + # Get database session + db = get_db_session() + + if db is None: + logger.error("Failed to create database session") + return False + + # Create all tables if they don't exist + from th_agenter.db.database import engine as global_engine + if global_engine: + Base.metadata.create_all(bind=global_engine) + + logger.info("Starting hardcoded resources migration...") + + # Check if resources already exist + existing_count = db.query(Resource).count() + if existing_count > 0: + logger.info(f"Found {existing_count} existing resources. Checking role assignments.") + # 即使资源已存在,也要检查并分配角色资源关联 + admin_role = db.query(Role).filter(Role.name == "系统管理员").first() + if admin_role: + # 获取所有资源 + all_resources = db.query(Resource).all() + assigned_count = 0 + + for resource in all_resources: + # 检查关联是否已存在 + existing = db.query(RoleResource).filter( + RoleResource.role_id == admin_role.id, + RoleResource.resource_id == resource.id + ).first() + + if not existing: + role_resource = RoleResource( + role_id=admin_role.id, + resource_id=resource.id + ) + db.add(role_resource) + assigned_count += 1 + + if assigned_count > 0: + db.commit() + logger.info(f"已为系统管理员角色分配 {assigned_count} 个新资源") + else: + logger.info("系统管理员角色已拥有所有资源") + else: + logger.warning("未找到系统管理员角色") + + return True + + # Define hardcoded resource data + main_menu_data = [ + { + "name": "智能问答", + "code": "CHAT", + "type": "menu", + "path": "/chat", + "component": "views/Chat.vue", + "icon": "ChatDotRound", + "description": "智能问答功能", + "sort_order": 1, + "requires_auth": True, + "requires_admin": False + }, + { + "name": "智能问数", + "code": "SMART_QUERY", + "type": "menu", + "path": "/smart-query", + "component": "views/SmartQuery.vue", + "icon": "DataAnalysis", + "description": "智能问数功能", + "sort_order": 2, + "requires_auth": True, + "requires_admin": False + }, + { + "name": "知识库", + "code": "KNOWLEDGE", + "type": "menu", + "path": "/knowledge", + "component": "views/KnowledgeBase.vue", + "icon": "Collection", + "description": "知识库管理", + "sort_order": 3, + "requires_auth": True, + "requires_admin": False + }, + { + "name": "工作流编排", + "code": "WORKFLOW", + "type": "menu", + "path": "/workflow", + "component": "views/Workflow.vue", + "icon": "Connection", + "description": "工作流编排功能", + "sort_order": 4, + "requires_auth": True, + "requires_admin": False + }, + { + "name": "智能体管理", + "code": "AGENT", + "type": "menu", + "path": "/agent", + "component": "views/Agent.vue", + "icon": "User", + "description": "智能体管理功能", + "sort_order": 5, + "requires_auth": True, + "requires_admin": False + }, + { + "name": "系统管理", + "code": "SYSTEM", + "type": "menu", + "path": "/system", + "component": "views/SystemManagement.vue", + "icon": "Setting", + "description": "系统管理功能", + "sort_order": 6, + "requires_auth": True, + "requires_admin": True + } + ] + + # Create main menu resources + created_resources = {} + for menu_data in main_menu_data: + resource = Resource(**menu_data) + db.add(resource) + db.flush() + created_resources[menu_data["code"]] = resource + logger.info(f"Created main menu resource: {menu_data['name']}") + + # System management submenu data + system_submenu_data = [ + { + "name": "用户管理", + "code": "SYSTEM_USERS", + "type": "menu", + "path": "/system/users", + "component": "components/system/UserManagement.vue", + "icon": "User", + "description": "用户管理功能", + "parent_id": created_resources["SYSTEM"].id, + "sort_order": 1, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "部门管理", + "code": "SYSTEM_DEPARTMENTS", + "type": "menu", + "path": "/system/departments", + "component": "components/system/DepartmentManagement.vue", + "icon": "OfficeBuilding", + "description": "部门管理功能", + "parent_id": created_resources["SYSTEM"].id, + "sort_order": 2, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "角色管理", + "code": "SYSTEM_ROLES", + "type": "menu", + "path": "/system/roles", + "component": "components/system/RoleManagement.vue", + "icon": "Avatar", + "description": "角色管理功能", + "parent_id": created_resources["SYSTEM"].id, + "sort_order": 3, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "权限管理", + "code": "SYSTEM_PERMISSIONS", + "type": "menu", + "path": "/system/permissions", + "component": "components/system/PermissionManagement.vue", + "icon": "Lock", + "description": "权限管理功能", + "parent_id": created_resources["SYSTEM"].id, + "sort_order": 4, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "资源管理", + "code": "SYSTEM_RESOURCES", + "type": "menu", + "path": "/system/resources", + "component": "components/system/ResourceManagement.vue", + "icon": "Grid", + "description": "资源管理功能", + "parent_id": created_resources["SYSTEM"].id, + "sort_order": 5, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "大模型管理", + "code": "SYSTEM_LLM_CONFIGS", + "type": "menu", + "path": "/system/llm-configs", + "component": "components/system/LLMConfigManagement.vue", + "icon": "Cpu", + "description": "大模型配置管理", + "parent_id": created_resources["SYSTEM"].id, + "sort_order": 6, + "requires_auth": True, + "requires_admin": True + } + ] + + # Create system management submenu + for submenu_data in system_submenu_data: + submenu = Resource(**submenu_data) + db.add(submenu) + db.flush() + created_resources[submenu_data["code"]] = submenu + logger.info(f"Created system submenu resource: {submenu_data['name']}") + + # Button resources data + button_resources_data = [ + # User management buttons + { + "name": "新增用户", + "code": "USER_CREATE_BTN", + "type": "button", + "description": "新增用户按钮", + "parent_id": created_resources["SYSTEM_USERS"].id, + "sort_order": 1, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "编辑用户", + "code": "USER_EDIT_BTN", + "type": "button", + "description": "编辑用户按钮", + "parent_id": created_resources["SYSTEM_USERS"].id, + "sort_order": 2, + "requires_auth": True, + "requires_admin": True + }, + # Role management buttons + { + "name": "新增角色", + "code": "ROLE_CREATE_BTN", + "type": "button", + "description": "新增角色按钮", + "parent_id": created_resources["SYSTEM_ROLES"].id, + "sort_order": 1, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "编辑角色", + "code": "ROLE_EDIT_BTN", + "type": "button", + "description": "编辑角色按钮", + "parent_id": created_resources["SYSTEM_ROLES"].id, + "sort_order": 2, + "requires_auth": True, + "requires_admin": True + }, + # Permission management buttons + { + "name": "新增权限", + "code": "PERMISSION_CREATE_BTN", + "type": "button", + "description": "新增权限按钮", + "parent_id": created_resources["SYSTEM_PERMISSIONS"].id, + "sort_order": 1, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "编辑权限", + "code": "PERMISSION_EDIT_BTN", + "type": "button", + "description": "编辑权限按钮", + "parent_id": created_resources["SYSTEM_PERMISSIONS"].id, + "sort_order": 2, + "requires_auth": True, + "requires_admin": True + } + ] + + # Create button resources + for button_data in button_resources_data: + button = Resource(**button_data) + db.add(button) + db.flush() + created_resources[button_data["code"]] = button + logger.info(f"Created button resource: {button_data['name']}") + + # API resources data + api_resources_data = [ + # User management APIs + { + "name": "用户列表API", + "code": "USER_LIST_API", + "type": "api", + "path": "/api/users", + "description": "获取用户列表API", + "sort_order": 1, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "创建用户API", + "code": "USER_CREATE_API", + "type": "api", + "path": "/api/users", + "description": "创建用户API", + "sort_order": 2, + "requires_auth": True, + "requires_admin": True + }, + # Role management APIs + { + "name": "角色列表API", + "code": "ROLE_LIST_API", + "type": "api", + "path": "/api/admin/roles", + "description": "获取角色列表API", + "sort_order": 5, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "创建角色API", + "code": "ROLE_CREATE_API", + "type": "api", + "path": "/api/admin/roles", + "description": "创建角色API", + "sort_order": 6, + "requires_auth": True, + "requires_admin": True + }, + # Resource management APIs + { + "name": "资源列表API", + "code": "RESOURCE_LIST_API", + "type": "api", + "path": "/api/admin/resources", + "description": "获取资源列表API", + "sort_order": 10, + "requires_auth": True, + "requires_admin": True + }, + { + "name": "创建资源API", + "code": "RESOURCE_CREATE_API", + "type": "api", + "path": "/api/admin/resources", + "description": "创建资源API", + "sort_order": 11, + "requires_auth": True, + "requires_admin": True + } + ] + + # Create API resources + for api_data in api_resources_data: + api_resource = Resource(**api_data) + db.add(api_resource) + db.flush() + created_resources[api_data["code"]] = api_resource + logger.info(f"Created API resource: {api_data['name']}") + + # 分配资源给系统管理员角色 + admin_role = db.query(Role).filter(Role.name == "系统管理员").first() + if admin_role: + all_resources = list(created_resources.values()) + for resource in all_resources: + # 检查关联是否已存在 + existing = db.query(RoleResource).filter( + RoleResource.role_id == admin_role.id, + RoleResource.resource_id == resource.id + ).first() + + if not existing: + role_resource = RoleResource( + role_id=admin_role.id, + resource_id=resource.id + ) + db.add(role_resource) + + logger.info(f"已为系统管理员角色分配 {len(all_resources)} 个资源") + else: + logger.warning("未找到系统管理员角色") + + db.commit() + + total_resources = db.query(Resource).count() + logger.info(f"Migration completed successfully. Total resources: {total_resources}") + + return True + + except Exception as e: + logger.error(f"Migration failed: {str(e)}") + if db: + db.rollback() + return False + finally: + if db: + db.close() + +def main(): + """Main function to run the migration.""" + print("=== 硬编码资源数据迁移 ===") + success = migrate_hardcoded_resources() + if success: + print("\n🎉 资源数据迁移完成!") + else: + print("\n❌ 资源数据迁移失败!") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/backend/th_agenter/db/migrations/remove_permission_tables.py b/backend/th_agenter/db/migrations/remove_permission_tables.py new file mode 100644 index 0000000..9f514ef --- /dev/null +++ b/backend/th_agenter/db/migrations/remove_permission_tables.py @@ -0,0 +1,146 @@ +"""删除权限相关表的迁移脚本 + +Revision ID: remove_permission_tables +Revises: add_system_management +Create Date: 2024-01-25 10:00:00.000000 + +""" +from alembic import op +import sqlalchemy as sa +from sqlalchemy import text + + +# revision identifiers, used by Alembic. +revision = 'remove_permission_tables' +down_revision = 'add_system_management' +branch_labels = None +depends_on = None + + +def upgrade(): + """删除权限相关表.""" + + # 获取数据库连接 + connection = op.get_bind() + + # 删除外键约束和表(按依赖关系顺序) + tables_to_drop = [ + 'user_permissions', # 用户权限关联表 + 'role_permissions', # 角色权限关联表 + 'permission_resources', # 权限资源关联表 + 'permissions', # 权限表 + 'role_resources', # 角色资源关联表 + 'resources', # 资源表 + 'user_departments', # 用户部门关联表 + 'departments' # 部门表 + ] + + for table_name in tables_to_drop: + try: + # 检查表是否存在 + result = connection.execute(text(f""" + SELECT EXISTS ( + SELECT FROM information_schema.tables + WHERE table_name = '{table_name}' + ); + """)) + table_exists = result.scalar() + + if table_exists: + print(f"删除表: {table_name}") + op.drop_table(table_name) + else: + print(f"表 {table_name} 不存在,跳过") + + except Exception as e: + print(f"删除表 {table_name} 时出错: {e}") + # 继续删除其他表 + continue + + # 删除用户表中的部门相关字段 + try: + # 检查字段是否存在 + result = connection.execute(text(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name = 'users' AND column_name = 'department_id'; + """)) + column_exists = result.fetchone() + + if column_exists: + print("删除用户表中的 department_id 字段") + op.drop_column('users', 'department_id') + else: + print("用户表中的 department_id 字段不存在,跳过") + + except Exception as e: + print(f"删除 department_id 字段时出错: {e}") + + # 简化 user_roles 表结构(如果需要的话) + try: + # 检查 user_roles 表是否有多余的字段 + result = connection.execute(text(""" + SELECT column_name + FROM information_schema.columns + WHERE table_name = 'user_roles' AND column_name IN ('id', 'created_at', 'updated_at', 'created_by', 'updated_by'); + """)) + extra_columns = [row[0] for row in result.fetchall()] + + if extra_columns: + print("简化 user_roles 表结构") + # 创建新的简化表 + op.execute(text(""" + CREATE TABLE user_roles_new ( + user_id INTEGER NOT NULL, + role_id INTEGER NOT NULL, + PRIMARY KEY (user_id, role_id), + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + FOREIGN KEY (role_id) REFERENCES roles(id) ON DELETE CASCADE + ); + """)) + + # 迁移数据 + op.execute(text(""" + INSERT INTO user_roles_new (user_id, role_id) + SELECT DISTINCT user_id, role_id FROM user_roles; + """)) + + # 删除旧表,重命名新表 + op.drop_table('user_roles') + op.execute(text("ALTER TABLE user_roles_new RENAME TO user_roles;")) + + except Exception as e: + print(f"简化 user_roles 表时出错: {e}") + + +def downgrade(): + """回滚操作 - 重新创建权限相关表.""" + + # 注意:这是一个破坏性操作,回滚会丢失数据 + # 在生产环境中应该谨慎使用 + + print("警告:回滚操作会重新创建权限相关表,但不会恢复数据") + + # 重新创建基本的权限表结构(简化版) + op.create_table('permissions', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(100), nullable=False), + sa.Column('code', sa.String(100), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('is_active', sa.Boolean(), nullable=False, default=True), + sa.Column('created_at', sa.DateTime(), nullable=True), + sa.Column('updated_at', sa.DateTime(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('code') + ) + + op.create_table('role_permissions', + sa.Column('role_id', sa.Integer(), nullable=False), + sa.Column('permission_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('role_id', 'permission_id') + ) + + # 添加用户表的 department_id 字段 + op.add_column('users', sa.Column('department_id', sa.Integer(), nullable=True)) \ No newline at end of file diff --git a/backend/th_agenter/main.py b/backend/th_agenter/main.py new file mode 100644 index 0000000..424e126 --- /dev/null +++ b/backend/th_agenter/main.py @@ -0,0 +1,35 @@ +from dotenv import load_dotenv +import os +import sys +from pathlib import Path + +# Add backend directory to Python path for direct execution +if __name__ == "__main__": + backend_dir = Path(__file__).parent.parent + if str(backend_dir) not in sys.path: + sys.path.insert(0, str(backend_dir)) + +# Load environment variables from .env file +load_dotenv() + +from th_agenter.core.app import create_app + +# Create FastAPI application using factory function +app = create_app() + +# 在 main.py 中添加表元数据路由 + + + +if __name__ == "__main__": + import uvicorn + import argparse + + parser = argparse.ArgumentParser(description='TH-Agenter Backend Server') + parser.add_argument('--host', default='0.0.0.0', help='Host to bind to') + parser.add_argument('--port', type=int, default=8000, help='Port to bind to') + parser.add_argument('--reload', action='store_true', help='Enable auto-reload') + + args = parser.parse_args() + + uvicorn.run(app, host=args.host, port=args.port, reload=args.reload) \ No newline at end of file diff --git a/backend/th_agenter/models/__init__.py b/backend/th_agenter/models/__init__.py new file mode 100644 index 0000000..c95acf6 --- /dev/null +++ b/backend/th_agenter/models/__init__.py @@ -0,0 +1,27 @@ +"""Database models for TH-Agenter.""" + +from .user import User +from .conversation import Conversation +from .message import Message +from .knowledge_base import KnowledgeBase, Document +from .agent_config import AgentConfig +from .excel_file import ExcelFile +from .permission import Role, UserRole +from .llm_config import LLMConfig +from .workflow import Workflow, WorkflowExecution, NodeExecution + +__all__ = [ + "User", + "Conversation", + "Message", + "KnowledgeBase", + "Document", + "AgentConfig", + "ExcelFile", + "Role", + "UserRole", + "LLMConfig", + "Workflow", + "WorkflowExecution", + "NodeExecution" +] \ No newline at end of file diff --git a/backend/th_agenter/models/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/models/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9720d424c2687ac397339dad593014d72c77ad23 GIT binary patch literal 796 zcmZ8fO>fgc5M3uu?AUSAmNsyxN;niLhZH1k2%%CMgw|~(2#8ChmFsMR6@NryhqT>`te2kWFkCyA52N!EV?Ri&7HYkgpK<%b z`V3g#=$Cwhm!Q-pUiQn}1XFX*yLNV#aqy7vj?p2zB1tUxZd$T-aQP8;55i(JIRJJ&fB95 z+HYQVpN(Z&NZ9*jpy!UE%(uA&-0RP?^n-*vD55MyZ5_)z55}@nQl6h?^H>hY@&M~8 z`!EY<30A=z#!%ei6l#X1>m1jHdd;==0Ih@pWuj+3zx#jN)vr)>o`| zCc_!sue=j9{a1E8D5G022`U6t0*k;Vs1eXRflE*)@CX_NO@x!q9c2kI%t9fQIYUpn zq^gJiw5=~tR&v$3zDYGEtL6nw^X6t%$LXYnzyjF3_Msd}{fiL}KzmZoR6-KvIrenp zPdRa%tn$dKvgg;c_|G+24&qpduWX5NOda$d53;FD_1L>f5T%3SeODWiFgOjuiA;xh zBvwaPk3SG1N~1!Ez3I8KpWxvvmQP^^OVV$UcM&ejhGBeXyFXcL!R{~E_62*iV7m); V?}8-a{x`P&+ibX{PZn%r#vfuc*+KvS literal 0 HcmV?d00001 diff --git a/backend/th_agenter/models/__pycache__/agent_config.cpython-313.pyc b/backend/th_agenter/models/__pycache__/agent_config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2159ffbcc97fc698e3eaddfa46096946ecbcf55f GIT binary patch literal 2392 zcmZuzO-vg{6rT0p`Ul$>%zq$GLgKgt6B>v_6p)(IMl6C#S<*_8)79b~$SU@(GrLX$ zid3I^231jratJ-uxzST|>AgJ_pMa4nwW_L$TT+@!PJM4|2vA1W+xOo0=FPspS?@)o zVFD{M`PaROOvs-o>@Rqi5lo8s+?;Y_11l}=wdJvde{BYBDLI9-nJIZ#WK@f z)3#DY+n|N)E*fTJ9{AWUl#3RZr!1E)QpN+S%S>yL`{w8)m&-S7yFhh|2Pbux&Y4BZ zeTw?oY)0a~g|e03^`IMxhi>Q&ok7p}`Ww_~pdswN1Lg_I5hg*n9#vvq)x&(Mm-%yn zvm_@&KoHWHmj#6!!jML#`fe4`AYfsEML-ITd03N>q7ZgW4Fei)c#5b^fHoiai5&Qe z3M6sFON)?N!6)uY9_)I5dQ2c~0+H3YklK&z>S#!^z&h3DZo<0M1ki4^h4n19W|BN2 zO7XF%#BLA*W0*M})v-cay+Y1h#=|2eW)^j}q}``W+;3V2eY9(U>8F4Nt#YBD&lf0b z1x-d`ZTRqr2bT@HpqC5oE_NPZ?eJLkUd|3LM;A-(;Fav?pj&41wn^ald?UADLkKh| z!N%G990)X|ber4)?)Zuk)dL8=_pt?t7ZBeuHHd)tuE_l)s!~{6G!>P}fmUvLp7%eePkIjKa6b*yt@1I$gdN>Ol&H5Ht*y%<99cMclW^jXs+X+5V@X(0G^PO zGktQFPtGuC_ooaGYpikUVuLfFUMVt!Ct=$iTc8ye)loHQn~rKw0&VO>ZS#rRmJ_wf z6Sdf@#}=H{c}xW&XuJQfRi$hUdd#!j4g>a(GB@SgDZ_+jvMm^;?8_MIf?W)^@`yp5 zJTprO@+h@%o(#=}d(PppqW(xTU0S~?9UgIMv4m2YQ66)a9FVmlbsXp&F2hOlwnKSX zG?XSfHH;oy8(Pj`J$PIgTDLKoJe+3~u2DnNT^=r%j0Vrn;me%@QtxWkhZ|+r)-VR% zdbk@K%6>Hd8c2n_4v^N4r|H%7cCz#7)aulB&*`n6fm+YNcJG<3-l1CW&{ps0#)sQ| zXSe!>YkkA-@dHgq&(p`NkGH$~UJN}O+JAKQzUX__w=W&R-t@+s!>>s^6lORTFUf4? z6pjyO0LCY*X^@d}0r@6Pdr;O34J58<3np{if@x9923XcK!_ET})if6?Q8#3Gl`#BJ z)(eLnE~>}yu3{KH!@lLdg6X*IJjxf4q>*6jGi-L2LGl3-99&Tlb_u!DNQQv$HcfL} z_%i09DDdT*FT2!%ksn3_3{Dh-;n)g-Zk?Zid_#78{l6#ARc8JO#aHC@xs7BkbhUD0 zCz4pbu-5wRK-Ir7SBqS$OzyOFt|hC!TFYQ%`b~he^{kCl&%a96k`tAg9guETZ`Il^ zS7yQPS^K)@d3W`8t>sE(8nMahNF9qMSH4&`YO#UJP4EGhfi>rOq^i{t;}vBm(Xl31 zleI*)q5zucSosRhYaXmj)sePZVyL406OjC0N^b)Rf4yuXp4m){U4?LX*m+RU3;BDr zxO6D{BJm>UBA+2lgGhYry?GW`7C|4fF`u1>KLRX@b|Pm-^T>nO>zy&d#=!)xK;mHY kzxGO!v_n#VlbJ0tQzJ9Kk<;+|H!vuLS2Ax2JVY`60~A3mG5`Po literal 0 HcmV?d00001 diff --git a/backend/th_agenter/models/__pycache__/conversation.cpython-313.pyc b/backend/th_agenter/models/__pycache__/conversation.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ca552133dff325abeccbc9e4c219fc558788ede5 GIT binary patch literal 2162 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 F{{c7iP=Npd literal 0 HcmV?d00001 diff --git a/backend/th_agenter/models/__pycache__/excel_file.cpython-313.pyc b/backend/th_agenter/models/__pycache__/excel_file.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..eb5da68d0c9dd11c1cb5e80c6e8e3304b6e74a69 GIT binary patch literal 4211 zcma)9O>h(05q_h8%d#X}60(K862=4zY{_7oh2_VF#VnTBW?^h_Kw&bL#rWqs?;Xsn;pYmlkS<3YztLwUX7k!zwX!b zy8C;zit)t{EjPD1{PM6H?i!R}ocJSQxngk@O66caO0L(E8xF%vb#%+wsS zP)p28tuY(5g;2&g)e^H)JB&@Nd8#$$ppFpfB)x>S1PE)5J4TI~ZrZkF3%BHZC%#Px z>Cj>-EreFGNg*wU7PB-YuJBX}J4#>F=Q0+RUNp>8dJO?PRO8HpaSL-<=J*3ippwXI6mFm)| z0P6@4dN|(Awgt$jW;UTcKs)!*pRr!n9S@Ee*!BQy-N@kw6dBTB1)DB%z#;?{p*`85cnEt5;9`GvHQ`T*hB zIt|lXN$6(72F02b7WrISq8%`i(4z%ILagvZ2=3m74xr7h|`hk{9!i0oA4qzam^bo^L%q5Xy-H<&b3vUkV!)Ln^7X zgYiqL46j*=vWis~`w>tq3h6}=Z3!vYC%%YhSB1>wB_SgXuJEZ$Be}pQekWv->NLlr zSX?__@jYKgR@W38$5COGa-7mp-*KciHvlgaPeE58)%M^^)1R$>vKH4$j*Z#M=#)G< zRUY~JFH`0Dcx65*&nL@wgtD+yb}g4}%e%1XpySbDs^dps@SNPM2c@~2S!S>m4wzw7 z4*|H4qNE$Z8R9*NG}2?1ipCwk;$)Sm{3t&mY96BNfk4*>iN$)mOHSz2c!$ zJusu#_gS-duGI)dC zfGaD*?7bJ|=iSXTo(C}U@LMv>VLm{pHnjRX0Yl9tZeeC=Xmd=I)Bt<~#<0@XAOd8A z?j{odr*8E^Kd8S!cN2-`x`+1ddkFXKIH(t?2Hj00n(JnnJ)fxg7s`*uR@eQR>hC5J zzoeTrMl5)6wEwUve+CavDg))k3NNLy8TG>9L*lBCNG+xk+Gz?w@uA-x+JttxX2KSF z0`{m_6KWA6DvnhuJW2_VIlQ41JNBHkwhAZDE@dT1;WYbLRNAz$z9?){+H~cddT8jc zP^Uv@bGSLhxFkq)5awwFJIq9M2)eL=9>y_lLqyDnLRvy|!U9d5rl520Tp#3|!<9cn zS0Mj#byd51DqSb#u9KCnh1XK$t2^*)m?}f#^3eF}_P-8&S^f>b9s|MR-AZ&!j*e~i zj8#2;ND7CKRmZPZ#&5#&RI2#-?-km z8kk4k1}>E6Zdb3xD%a-WDb7}UqH<5P(ladg3~$6oV}8r* zO?;ZL>lx}jIOUfTb`pFZC4KONoYJ&r@Q~}*3CUpHP*3cjSZ{NNS~|}d=ZrOcqt@vj zz&LZOR&>`E!2g`#UOlVK8MUld(*m>sm(=E2BWp52MH;DBry(*Og90D(C|)0y{Bb-1 zVr~VB>$Oi$LONZ)Q1ISYkvFr286#}cz~5E_s&;|c5#pRWrnmyc5YH5^5XHi;LVc2? z$2A5*SF!5v*0kyO27+G1j2nWcA^H&FmQSZOREwOxObH!C_NNkHE)mzDE0FgtPsJ64 zr|R*oPrmQ)u3M{t&nkfvZv!XRV=DLu-nnN|Go2>v)ViGw`2XK zz{aE;zO;F44Cqo^?){<~441-kaAbqsSSSb2?X;Lf9X}IuyJyEr4xilaAa4J4QoZ{fPhdDm2b6gyhwN#YSsf>`x0^7=Q$!r1!Z5$_I zd8-LhgcLi|oD(R`Qi`WVX-o4W#3rGO0&T$7IhqXz=$GlcibNf4Xe)?%0U$aPe#@DSL+t)BiCs<`e^2)Z*MR z7|dsPjWF8A(Y8&W8)xd1~)#_n!nIhGhrva3ja`nJxufI#Imrm zwhtyMEHIX8j5R>0F?8;UM_@?th{G4tLSzBHj458@S|koeN3mVdAbg451Q7@h5syK) mZDbhc9qIo!nXZs&nN0tk1b-sI50-Bj#{abCBf)=l)BghzQL%9V literal 0 HcmV?d00001 diff --git a/backend/th_agenter/models/__pycache__/knowledge_base.cpython-313.pyc b/backend/th_agenter/models/__pycache__/knowledge_base.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..559e02e538905561e1c2cfb1ee2b1c0b5e2eaa4b GIT binary patch literal 4695 zcmcIoO>EoP5hnFZS(YhVlI>*k!~CzXv7E+n^RtQT#PUz%pQND^AXsAwnR>EWQ>5}p zIf=4Z=v1_)CddYHkVCzDSqJFBd)ffKsz)?4?VRrq$JCZ(=7@t z0gv9infLhS=goZdMkLb0z!Oe?H-93+Fn>j+{`hNy4HE{B7?Gij$cmnEHshh5jF);d zKI+T(sXr5-flQDFGaThIAsP};&pY0d3DYo)eWHK7H4~u`f$3%rGGbsiBL-(8{a$x} z+UCr1FdJ%`<>y+G;d1A=rr%acZjK1ES%U}#Jx5d{wT?@ZoE;d_)lxyT1ENV4ZO#r} z)=V--sNFK6Q=-giXeyOk0(Ybv^C%qzup z5AMs_p-b?xNqm>=9@3@38nf{l29KB-hO*!lkH}K5=%GH*OZ_u}gUn12tn~G>uD0kG z1Mn9->!X|_4FS)c_0X0bbwgsySuYJcvQ}6ZcKAqxZ*}-Khv%K0BcLB`?A6xvUf$7d zZ^$;!)H?YGq5AA{Vow)|1?N1vcdO(7CvF!ngx6gl=N&2SB9p@WfK+n9d zs4y9=z1p52V8Usr}G$`pePJ zN0-m8jIB<8eeDxU=2?% zV|ZfB804Sce$599w>|II_#7nEB&7Rw_Nr%c7ZhMdfR!4%=`+LQ&g9X1=ic^SRyLJI z@;^AyOOL`!=`kR7t71q+s>{SMNDgk0fRkSEl7R^>2(D8T?%vW?@7ag8`gTX9L$HnU z0g#%Dmrs2;@WsH(*vJ*^Nnz7X^~IMUo)ZODc|ACFxcvtJWmEB;^%qn5v=?O$TvM!V|+FB1tAz6jzaA z5GlrjVggVs;&wPk44EoLEc`Z4ASH7-Xdw75qEf64c8fe;(iS8`DHA*5j`c-C)ohXC zf$b3Z()EGewn$`Cr&4ZKGVd0N&C9x~!W3-o*g+5C0uDT1B-Ff1F$-u9&Lt$tFrod( zQr3hPHd``@0T;Cw6)+>6fKVPsNU-7>zX$S5X3e+%+xU^n9vmTrTB`^>gcJAuXXNua$|Yu=|`*m)l0vyIu|Ne*4n$4f-6U!$F25(N@gv- zd+GGb_2)xYe6TXP7VW%$d&Rd}vZ5C%m%k4%uzY0spj5pP5fTq zq02YA`##{CyZwH#g9>x8WZdS`wSv5!ggBJ>I_rY2lv6=QXeY?l{|!WW1A(HuVMIHC zOj9Uh0EU2UDWiDZ@CI(&0P_F#S!ggj6jSKHzP=!|7ync_a=w0Cr}?%X7xIc*0iXeg zjvW^kW+m*(9%#j0x);u3b9D{7z0r6aumXzP8;yH|+w1RWG}BSo$iSP7&2-woPnZE@ z(vR+j2Ylcy$FWJEO#;>|Y^sx&QVPRF8QKau&`tdmnqo@kp=>NZ^VIBiDzsnKZxi~q z`B2kw7w9Z1X)?cg`U)Jwzyzu=Q1qhl-+cP(PhateEdJ0-{zx@BXz_zpZtzF(z`3!S z2ZVQc;A(orbpY60%;o7M9P=$B!w3R$dDq5xBg9R!(`e$JcbK@+Y4N>PuJ=c=uo2_K zJ6os;SMqs9CXF!r5#!w{`%`yR1AmG|^Uls@w}FAWv)Hw~tJ(41$rQ8K1t5qi=r9tz>og6-Zr7nWgS1COo~{+ZUyg11!87%Su<;g zT^Qp)WqKOrEj8q#WA1p4( z5AUs9x1xQO%PtH_SkYc*46@mWWmp_NS?5uA+Tj7D0E9WV(*L~Qie0Qstaa^O%7Luw zWYrf3ArBR5Ic>EiE2C?%T}y{o;#MqGnE(lJT`LDxiPdqwG7fy#-WpVLkqzP`-?C}7 zAFpH(<;-oOoHGrSbEbiEF1jcOq0dFOF?*t+d7?2lyorZ~Yj_A N)!lzG`0HfHe*np4X}kac literal 0 HcmV?d00001 diff --git a/backend/th_agenter/models/__pycache__/llm_config.cpython-313.pyc b/backend/th_agenter/models/__pycache__/llm_config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..a306a38a2f58bf54ee7dbda30e47440e8e4ab3d2 GIT binary patch literal 7259 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 literal 0 HcmV?d00001 diff --git a/backend/th_agenter/models/__pycache__/message.cpython-313.pyc b/backend/th_agenter/models/__pycache__/message.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8ad325649a7ba1aaf0ee84f05265c60aeac8d285 GIT binary patch literal 3525 zcmcImO>7(25q?W9$>qOfQL-erBBKBl>(haP(9fvgNzdi_C*HZ62hDz^=K>dbOUOR5Ct z(g$Mr=FRMTZ@!r~GrPf{kHGLxe7*b|fsnr-(;RMlvil`W9ukpIB66Z*noByUBk82h zBv1LIi@K6->P~v7Cn-=NiaO3|Z_-D7)|{XAQ@`jE-P3_&kOrfqo4iUyPcIRLbSUkg z4>gZ_@jt>h<}_^SdO_FsQr$L7*AKdZba2FJ@6ev(W5NG~W);Jbb4oN{$SPWV8-+0s zb6+cHrM%AEqDfUf$2?QIspJ&Oe3J#Ls5yOFDKkE$d}uO0p_lTEPl`9@VjK%lMUzdn zpc~6-ac^#4PKSBsk1eGm>lqdW$7fF(ipb;I6x9FiRvZrwc_zAI~Yec|I?z`ayP4 z&fHb>thFqGcXTst`Nv4-8}5o1%QOsoSp-oqaQmN-E&j}=zr8ZC*><8bwHfUEc#@uK zsvQpfSrES*deK9@IRP+h!k;P^mF;%;_67VVEFFN~!~k)rc`$+TORA`?xMq=mk*4-~zeG ziI6AW6W-%!ce+RP^^y_$WZDZnC;Le(#60%X+D1qzK4vD^5ok=p zyB744rpb$%5_2$5R#}ounn_;=wN%E2#5&0X6w}V`6Ce-CotA8Y%;Tm9MiVBHyZjj; z@-adV*ApS3@>8NRhn(em{Eoi;G{4{bFom z^!l%dH%4dI`ad6hG`Mb59kt$bRpqz67i!`=&qS#%O83K??I+i=pXVOs)^4pYSIwW7 zAD63lzv%mAp_aH=yLqb?zFiY;TQ4ND*Dr{k<-fnd=z*A6;sa2raZ^mEk}N6VrhW#qnic`7HJ`0G$peDncP5)d{I+yljc&uESR!p zb1^4#8KojrE00-_@Hply7K$;RjskeBP1Q46DXX;3WIUeCypojBpu9oJK_^9{(TWv4 z^ey}<+zcFhOAKS=;4?9;%xBZTj+r=A90+YEQy_cubQ^~~W!bhZ^jV=#G$4;uatjytWOJM6(; zzD@`@hFdvxZCT0OjjBsg+Y?dMh%N!#Q7rfIn3KK%J2&cMFU%fu7KjGk$9`M^)sXND z$!ZWHO`P_wUIA^ZlvK!$o3uqfDL*LxZ2DQ>U>$zp!J07m|F!bWkyf_anf<1oK%=Md z)_V+XZ<^Hlh&B7A`2UD7Xbv-ZsNOeJ4-eIZA?gYa z6ggFy{c~5}+B?-!y=$a$W6R(EY5(d+8^_L6mAe08WnwE7Sxv1c>Y>*vZ*O(R4t>ch}tEu|&;Tj+L#!Y%oubI_sz57Cq?*z8<#M&rG zBg2*1t?pB6_nt)R-EV*hHe4I1PSiVwDl`8O9IiXuzXh)=veQM}p>G@x*ZEy1%yw|L zBiL)h*4n>dZMfk(G%I$j(Pa6I_cd9|EGzkP!$X*=EjLjt+0^odm literal 0 HcmV?d00001 diff --git a/backend/th_agenter/models/__pycache__/permission.cpython-313.pyc b/backend/th_agenter/models/__pycache__/permission.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c1269785fdc420de60881d7552b38deb55dd37e7 GIT binary patch literal 2790 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 T4AB4j7X)eh=1&A3?)&@?WUk86 literal 0 HcmV?d00001 diff --git a/backend/th_agenter/models/__pycache__/user.cpython-313.pyc b/backend/th_agenter/models/__pycache__/user.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ea78cb3683a2fd9adbfce17df77eb5552ac85a67 GIT binary patch literal 5054 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 literal 0 HcmV?d00001 diff --git a/backend/th_agenter/models/__pycache__/workflow.cpython-313.pyc b/backend/th_agenter/models/__pycache__/workflow.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..33441dc2c97a1c419439fc03b2418946324174af GIT binary patch literal 8239 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" + + def to_dict(self): + """Convert to dictionary.""" + return { + "id": self.id, + "name": self.name, + "description": self.description, + "enabled_tools": self.enabled_tools or [], + "max_iterations": self.max_iterations, + "temperature": self.temperature, + "system_message": self.system_message, + "verbose": self.verbose, + "model_name": self.model_name, + "max_tokens": self.max_tokens, + "is_active": self.is_active, + "is_default": self.is_default, + "created_at": self.created_at, + "updated_at": self.updated_at + } \ No newline at end of file diff --git a/backend/th_agenter/models/base.py b/backend/th_agenter/models/base.py new file mode 100644 index 0000000..6312301 --- /dev/null +++ b/backend/th_agenter/models/base.py @@ -0,0 +1,5 @@ +"""Database base model.""" + +from sqlalchemy.ext.declarative import declarative_base + +Base = declarative_base() \ No newline at end of file diff --git a/backend/th_agenter/models/conversation.py b/backend/th_agenter/models/conversation.py new file mode 100644 index 0000000..d82f758 --- /dev/null +++ b/backend/th_agenter/models/conversation.py @@ -0,0 +1,38 @@ +"""Conversation model.""" + +from sqlalchemy import Column, String, Integer, ForeignKey, Text, Boolean +from sqlalchemy.orm import relationship + +from ..db.base import BaseModel + + +class Conversation(BaseModel): + """Conversation model.""" + + __tablename__ = "conversations" + + title = Column(String(200), nullable=False) + user_id = Column(Integer, nullable=False) # Removed ForeignKey("users.id") + knowledge_base_id = Column(Integer, nullable=True) # Removed ForeignKey("knowledge_bases.id") + system_prompt = Column(Text, nullable=True) + model_name = Column(String(100), nullable=False, default="gpt-3.5-turbo") + temperature = Column(String(10), nullable=False, default="0.7") + max_tokens = Column(Integer, nullable=False, default=2048) + is_archived = Column(Boolean, default=False, nullable=False) + + # Relationships removed to eliminate foreign key constraints + + def __repr__(self): + return f"" + + @property + def message_count(self): + """Get the number of messages in this conversation.""" + return len(self.messages) + + @property + def last_message_at(self): + """Get the timestamp of the last message.""" + if self.messages: + return self.messages[-1].created_at + return self.created_at \ No newline at end of file diff --git a/backend/th_agenter/models/database_config.py b/backend/th_agenter/models/database_config.py new file mode 100644 index 0000000..ad30432 --- /dev/null +++ b/backend/th_agenter/models/database_config.py @@ -0,0 +1,52 @@ +"""数据库配置模型""" + +from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, JSON +from sqlalchemy.sql import func +from ..db.base import BaseModel + + +# 在现有的DatabaseConfig类中添加关系 +from sqlalchemy.orm import relationship + +class DatabaseConfig(BaseModel): + """数据库配置表""" + __tablename__ = "database_configs" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(100), nullable=False) # 配置名称 + db_type = Column(String(20), nullable=False, unique=True) # 数据库类型:postgresql, mysql等 + host = Column(String(255), nullable=False) + port = Column(Integer, nullable=False) + database = Column(String(100), nullable=False) + username = Column(String(100), nullable=False) + password = Column(Text, nullable=False) # 加密存储 + is_active = Column(Boolean, default=True) + is_default = Column(Boolean, default=False) + connection_params = Column(JSON, nullable=True) # 额外连接参数 + + def to_dict(self, include_password=False, decrypt_service=None): + result = { + "id": self.id, + "created_by": self.created_by, + "name": self.name, + "db_type": self.db_type, + "host": self.host, + "port": self.port, + "database": self.database, + "username": self.username, + "is_active": self.is_active, + "is_default": self.is_default, + "connection_params": self.connection_params, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None + } + + # 如果需要包含密码且提供了解密服务 + if include_password and decrypt_service: + print('begin decrypt password') + result["password"] = decrypt_service._decrypt_password(self.password) + + return result + + # 添加关系 + # table_metadata = relationship("TableMetadata", back_populates="database_config") \ No newline at end of file diff --git a/backend/th_agenter/models/excel_file.py b/backend/th_agenter/models/excel_file.py new file mode 100644 index 0000000..da3c841 --- /dev/null +++ b/backend/th_agenter/models/excel_file.py @@ -0,0 +1,89 @@ +"""Excel file models for smart query.""" + +from sqlalchemy import Column, String, Integer, Text, Boolean, JSON, DateTime +from sqlalchemy.sql import func + +from ..db.base import BaseModel + + +class ExcelFile(BaseModel): + """Excel file model for storing file metadata.""" + + __tablename__ = "excel_files" + + + + # Basic file information + # user_id = Column(Integer, nullable=False) # 用户ID + original_filename = Column(String(255), nullable=False) # 原始文件名 + file_path = Column(String(500), nullable=False) # 文件存储路径 + file_size = Column(Integer, nullable=False) # 文件大小(字节) + file_type = Column(String(50), nullable=False) # 文件类型 (.xlsx, .xls, .csv) + + # Excel specific information + sheet_names = Column(JSON, nullable=False) # 所有sheet名称列表 + default_sheet = Column(String(100), nullable=True) # 默认sheet名称 + + # Data preview information + columns_info = Column(JSON, nullable=False) # 列信息:{sheet_name: [column_names]} + preview_data = Column(JSON, nullable=False) # 前5行数据:{sheet_name: [[row1], [row2], ...]} + data_types = Column(JSON, nullable=True) # 数据类型信息:{sheet_name: {column: dtype}} + + # Statistics + total_rows = Column(JSON, nullable=True) # 每个sheet的总行数:{sheet_name: row_count} + total_columns = Column(JSON, nullable=True) # 每个sheet的总列数:{sheet_name: column_count} + + # Processing status + is_processed = Column(Boolean, default=True, nullable=False) # 是否已处理 + processing_error = Column(Text, nullable=True) # 处理错误信息 + + # Upload information + # upload_time = Column(DateTime, default=func.now(), nullable=False) # 上传时间 + last_accessed = Column(DateTime, nullable=True) # 最后访问时间 + + def __repr__(self): + return f"" + + @property + def file_size_mb(self): + """Get file size in MB.""" + return round(self.file_size / (1024 * 1024), 2) + + @property + def sheet_count(self): + """Get number of sheets.""" + return len(self.sheet_names) if self.sheet_names else 0 + + def get_sheet_info(self, sheet_name: str = None): + """Get information for a specific sheet or default sheet.""" + if not sheet_name: + sheet_name = self.default_sheet or (self.sheet_names[0] if self.sheet_names else None) + + if not sheet_name or sheet_name not in self.sheet_names: + return None + + return { + 'sheet_name': sheet_name, + 'columns': self.columns_info.get(sheet_name, []) if self.columns_info else [], + 'preview_data': self.preview_data.get(sheet_name, []) if self.preview_data else [], + 'data_types': self.data_types.get(sheet_name, {}) if self.data_types else {}, + 'total_rows': self.total_rows.get(sheet_name, 0) if self.total_rows else 0, + 'total_columns': self.total_columns.get(sheet_name, 0) if self.total_columns else 0 + } + + def get_all_sheets_summary(self): + """Get summary information for all sheets.""" + if not self.sheet_names: + return [] + + summary = [] + for sheet_name in self.sheet_names: + sheet_info = self.get_sheet_info(sheet_name) + if sheet_info: + summary.append({ + 'sheet_name': sheet_name, + 'columns_count': len(sheet_info['columns']), + 'rows_count': sheet_info['total_rows'], + 'columns': sheet_info['columns'][:10] # 只显示前10列 + }) + return summary \ No newline at end of file diff --git a/backend/th_agenter/models/knowledge_base.py b/backend/th_agenter/models/knowledge_base.py new file mode 100644 index 0000000..32ead50 --- /dev/null +++ b/backend/th_agenter/models/knowledge_base.py @@ -0,0 +1,92 @@ +"""Knowledge base models.""" + +from sqlalchemy import Column, String, Integer, ForeignKey, Text, Boolean, JSON, Float +from sqlalchemy.orm import relationship + +from ..db.base import BaseModel + + +class KnowledgeBase(BaseModel): + """Knowledge base model.""" + + __tablename__ = "knowledge_bases" + + name = Column(String(100), unique=False, index=True, nullable=False) + description = Column(Text, nullable=True) + embedding_model = Column(String(100), nullable=False, default="sentence-transformers/all-MiniLM-L6-v2") + chunk_size = Column(Integer, nullable=False, default=1000) + chunk_overlap = Column(Integer, nullable=False, default=200) + is_active = Column(Boolean, default=True, nullable=False) + + # Vector database settings + vector_db_type = Column(String(50), nullable=False, default="chroma") + collection_name = Column(String(100), nullable=True) # For vector DB collection + + # Relationships removed to eliminate foreign key constraints + + def __repr__(self): + return f"" + + @property + def document_count(self): + """Get the number of documents in this knowledge base.""" + return len(self.documents) + + @property + def active_document_count(self): + """Get the number of active documents in this knowledge base.""" + return len([doc for doc in self.documents if doc.is_processed]) + + +class Document(BaseModel): + """Document model.""" + + __tablename__ = "documents" + + knowledge_base_id = Column(Integer, nullable=False) # Removed ForeignKey("knowledge_bases.id") + filename = Column(String(255), nullable=False) + original_filename = Column(String(255), nullable=False) + file_path = Column(String(500), nullable=False) + file_size = Column(Integer, nullable=False) # in bytes + file_type = Column(String(50), nullable=False) # .pdf, .txt, .docx, etc. + mime_type = Column(String(100), nullable=True) + + # Processing status + is_processed = Column(Boolean, default=False, nullable=False) + processing_error = Column(Text, nullable=True) + + # Content and metadata + content = Column(Text, nullable=True) # Extracted text content + doc_metadata = Column(JSON, nullable=True) # Additional metadata + + # Chunking information + chunk_count = Column(Integer, default=0, nullable=False) + + # Embedding information + embedding_model = Column(String(100), nullable=True) + vector_ids = Column(JSON, nullable=True) # Store vector database IDs for chunks + + # Relationships removed to eliminate foreign key constraints + + def __repr__(self): + return f"" + + @property + def file_size_mb(self): + """Get file size in MB.""" + return round(self.file_size / (1024 * 1024), 2) + + @property + def is_text_file(self): + """Check if document is a text file.""" + return self.file_type.lower() in ['.txt', '.md', '.csv'] + + @property + def is_pdf_file(self): + """Check if document is a PDF file.""" + return self.file_type.lower() == '.pdf' + + @property + def is_office_file(self): + """Check if document is an Office file.""" + return self.file_type.lower() in ['.docx', '.xlsx', '.pptx'] \ No newline at end of file diff --git a/backend/th_agenter/models/llm_config.py b/backend/th_agenter/models/llm_config.py new file mode 100644 index 0000000..cfe631e --- /dev/null +++ b/backend/th_agenter/models/llm_config.py @@ -0,0 +1,165 @@ +"""LLM Configuration model for managing multiple AI models.""" + +from sqlalchemy import Column, String, Text, Boolean, Integer, Float, JSON +from sqlalchemy.orm import relationship +from typing import Dict, Any, Optional +import json + +from ..db.base import BaseModel + + +class LLMConfig(BaseModel): + """LLM Configuration model for managing AI model settings.""" + + __tablename__ = "llm_configs" + + name = Column(String(100), nullable=False, index=True) # 配置名称 + provider = Column(String(50), nullable=False, index=True) # 服务商:openai, deepseek, doubao, zhipu, moonshot, baidu + model_name = Column(String(100), nullable=False) # 模型名称 + api_key = Column(String(500), nullable=False) # API密钥(加密存储) + base_url = Column(String(200), nullable=True) # API基础URL + + # 模型参数 + max_tokens = Column(Integer, default=2048, nullable=False) + temperature = Column(Float, default=0.7, nullable=False) + top_p = Column(Float, default=1.0, nullable=False) + frequency_penalty = Column(Float, default=0.0, nullable=False) + presence_penalty = Column(Float, default=0.0, nullable=False) + + # 配置信息 + description = Column(Text, nullable=True) # 配置描述 + is_active = Column(Boolean, default=True, nullable=False) # 是否启用 + is_default = Column(Boolean, default=False, nullable=False) # 是否为默认配置 + is_embedding = Column(Boolean, default=False, nullable=False) # 是否为嵌入模型 + + # 扩展配置(JSON格式) + extra_config = Column(JSON, nullable=True) # 额外配置参数 + + # 使用统计 + usage_count = Column(Integer, default=0, nullable=False) # 使用次数 + last_used_at = Column(String(50), nullable=True) # 最后使用时间 + + def __repr__(self): + return f"" + + def to_dict(self, include_sensitive=False): + """Convert to dictionary, optionally excluding sensitive data.""" + data = super().to_dict() + data.update({ + 'name': self.name, + 'provider': self.provider, + 'model_name': self.model_name, + 'base_url': self.base_url, + 'max_tokens': self.max_tokens, + 'temperature': self.temperature, + 'top_p': self.top_p, + 'frequency_penalty': self.frequency_penalty, + 'presence_penalty': self.presence_penalty, + 'description': self.description, + 'is_active': self.is_active, + 'is_default': self.is_default, + 'is_embedding': self.is_embedding, + 'extra_config': self.extra_config, + 'usage_count': self.usage_count, + 'last_used_at': self.last_used_at + }) + + if include_sensitive: + data['api_key'] = self.api_key + else: + # 只显示API密钥的前几位和后几位 + if self.api_key: + key_len = len(self.api_key) + if key_len > 8: + data['api_key_masked'] = f"{self.api_key[:4]}...{self.api_key[-4:]}" + else: + data['api_key_masked'] = "***" + else: + data['api_key_masked'] = None + + return data + + def get_client_config(self) -> Dict[str, Any]: + """获取用于创建客户端的配置.""" + config = { + 'api_key': self.api_key, + 'base_url': self.base_url, + 'model': self.model_name, + 'max_tokens': self.max_tokens, + 'temperature': self.temperature, + 'top_p': self.top_p, + 'frequency_penalty': self.frequency_penalty, + 'presence_penalty': self.presence_penalty + } + + # 添加额外配置 + if self.extra_config: + config.update(self.extra_config) + + return config + + def validate_config(self) -> Dict[str, Any]: + """验证配置是否有效.""" + if not self.name or not self.name.strip(): + return {"valid": False, "error": "配置名称不能为空"} + + if not self.provider or self.provider not in ['openai', 'deepseek', 'doubao', 'zhipu', 'moonshot', 'baidu']: + return {"valid": False, "error": "不支持的服务商"} + + if not self.model_name or not self.model_name.strip(): + return {"valid": False, "error": "模型名称不能为空"} + + if not self.api_key or not self.api_key.strip(): + return {"valid": False, "error": "API密钥不能为空"} + + if self.max_tokens <= 0 or self.max_tokens > 32000: + return {"valid": False, "error": "最大令牌数必须在1-32000之间"} + + if self.temperature < 0 or self.temperature > 2: + return {"valid": False, "error": "温度参数必须在0-2之间"} + + return {"valid": True, "error": None} + + def increment_usage(self): + """增加使用次数.""" + from datetime import datetime + self.usage_count += 1 + self.last_used_at = datetime.now().isoformat() + + @classmethod + def get_default_config(cls, provider: str, is_embedding: bool = False): + """获取服务商的默认配置模板.""" + templates = { + 'openai': { + 'base_url': 'https://api.openai.com/v1', + 'model_name': 'gpt-3.5-turbo' if not is_embedding else 'text-embedding-ada-002', + 'max_tokens': 2048, + 'temperature': 0.7 + }, + 'deepseek': { + 'base_url': 'https://api.deepseek.com/v1', + 'model_name': 'deepseek-chat' if not is_embedding else 'deepseek-embedding', + 'max_tokens': 2048, + 'temperature': 0.7 + }, + 'doubao': { + 'base_url': 'https://ark.cn-beijing.volces.com/api/v3', + 'model_name': 'doubao-lite-4k' if not is_embedding else 'doubao-embedding', + 'max_tokens': 2048, + 'temperature': 0.7 + }, + 'zhipu': { + 'base_url': 'https://open.bigmodel.cn/api/paas/v4', + 'model_name': 'glm-4' if not is_embedding else 'embedding-3', + 'max_tokens': 2048, + 'temperature': 0.7 + }, + 'moonshot': { + 'base_url': 'https://api.moonshot.cn/v1', + 'model_name': 'moonshot-v1-8k' if not is_embedding else 'moonshot-embedding', + 'max_tokens': 2048, + 'temperature': 0.7 + } + } + + return templates.get(provider, {}) \ No newline at end of file diff --git a/backend/th_agenter/models/message.py b/backend/th_agenter/models/message.py new file mode 100644 index 0000000..4ba2553 --- /dev/null +++ b/backend/th_agenter/models/message.py @@ -0,0 +1,69 @@ +"""Message model.""" + +from sqlalchemy import Column, String, Integer, ForeignKey, Text, Enum, JSON +from sqlalchemy.orm import relationship +import enum + +from ..db.base import BaseModel + + +class MessageRole(str, enum.Enum): + """Message role enumeration.""" + USER = "user" + ASSISTANT = "assistant" + SYSTEM = "system" + + +class MessageType(str, enum.Enum): + """Message type enumeration.""" + TEXT = "text" + IMAGE = "image" + FILE = "file" + AUDIO = "audio" + + +class Message(BaseModel): + """Message model.""" + + __tablename__ = "messages" + + conversation_id = Column(Integer, nullable=False) # Removed ForeignKey("conversations.id") + role = Column(Enum(MessageRole), nullable=False) + content = Column(Text, nullable=False) + message_type = Column(Enum(MessageType), default=MessageType.TEXT, nullable=False) + message_metadata = Column(JSON, nullable=True) # Store additional data like file info, tokens used, etc. + + # For knowledge base context + context_documents = Column(JSON, nullable=True) # Store retrieved document references + + # Token usage tracking + prompt_tokens = Column(Integer, nullable=True) + completion_tokens = Column(Integer, nullable=True) + total_tokens = Column(Integer, nullable=True) + + # Relationships removed to eliminate foreign key constraints + + def __repr__(self): + content_preview = self.content[:50] + "..." if len(self.content) > 50 else self.content + return f"" + + def to_dict(self, include_metadata=True): + """Convert to dictionary.""" + data = super().to_dict() + if not include_metadata: + data.pop('message_metadata', None) + data.pop('context_documents', None) + data.pop('prompt_tokens', None) + data.pop('completion_tokens', None) + data.pop('total_tokens', None) + return data + + @property + def is_from_user(self): + """Check if message is from user.""" + return self.role == MessageRole.USER + + @property + def is_from_assistant(self): + """Check if message is from assistant.""" + return self.role == MessageRole.ASSISTANT \ No newline at end of file diff --git a/backend/th_agenter/models/permission.py b/backend/th_agenter/models/permission.py new file mode 100644 index 0000000..a9fbf38 --- /dev/null +++ b/backend/th_agenter/models/permission.py @@ -0,0 +1,53 @@ +"""Role models for simplified RBAC system.""" + +from sqlalchemy import Column, String, Text, Boolean, ForeignKey, Integer +from sqlalchemy.orm import relationship +from typing import List, Dict, Any + +from ..db.base import BaseModel, Base + + +class Role(BaseModel): + """Role model for simplified RBAC system.""" + + __tablename__ = "roles" + + name = Column(String(100), nullable=False, unique=True, index=True) # 角色名称 + code = Column(String(100), nullable=False, unique=True, index=True) # 角色编码 + description = Column(Text, nullable=True) # 角色描述 + is_system = Column(Boolean, default=False, nullable=False) # 是否系统角色 + is_active = Column(Boolean, default=True, nullable=False) + + # 关系 - 只保留用户关系 + users = relationship("User", secondary="user_roles", back_populates="roles") + + def __repr__(self): + return f"" + + def to_dict(self): + """Convert to dictionary.""" + data = super().to_dict() + data.update({ + 'name': self.name, + 'code': self.code, + 'description': self.description, + 'is_system': self.is_system, + 'is_active': self.is_active + }) + return data + + +class UserRole(Base): + """User role association model.""" + + __tablename__ = "user_roles" + + user_id = Column(Integer, ForeignKey('users.id'), primary_key=True) + role_id = Column(Integer, ForeignKey('roles.id'), primary_key=True) + + # 关系 - 用于直接操作关联表的场景 + user = relationship("User", viewonly=True) + role = relationship("Role", viewonly=True) + + def __repr__(self): + return f"" \ No newline at end of file diff --git a/backend/th_agenter/models/table_metadata.py b/backend/th_agenter/models/table_metadata.py new file mode 100644 index 0000000..afa3926 --- /dev/null +++ b/backend/th_agenter/models/table_metadata.py @@ -0,0 +1,61 @@ +"""表元数据模型""" + +from sqlalchemy import Column, Integer, String, Text, DateTime, Boolean, JSON, ForeignKey +from sqlalchemy.sql import func +from sqlalchemy.orm import relationship +from ..db.base import BaseModel + + +class TableMetadata(BaseModel): + """表元数据表""" + __tablename__ = "table_metadata" + + id = Column(Integer, primary_key=True, index=True) + # database_config_id = Column(Integer, ForeignKey('database_configs.id'), nullable=False) + table_name = Column(String(100), nullable=False, index=True) + table_schema = Column(String(50), default='public') + table_type = Column(String(20), default='BASE TABLE') + table_comment = Column(Text, nullable=True) # 表描述 + database_config_id = Column(Integer, nullable=True) #数据库配置ID + # 表结构信息 + columns_info = Column(JSON, nullable=False) # 列信息:名称、类型、注释等 + primary_keys = Column(JSON, nullable=True) # 主键列表 + foreign_keys = Column(JSON, nullable=True) # 外键信息 + indexes = Column(JSON, nullable=True) # 索引信息 + + # 示例数据 + sample_data = Column(JSON, nullable=True) # 前5条示例数据 + row_count = Column(Integer, default=0) # 总行数 + + # 问答相关 + is_enabled_for_qa = Column(Boolean, default=True) # 是否启用问答 + qa_description = Column(Text, nullable=True) # 问答描述 + business_context = Column(Text, nullable=True) # 业务上下文 + + last_synced_at = Column(DateTime(timezone=True), nullable=True) # 最后同步时间 + + # 关系 + # database_config = relationship("DatabaseConfig", back_populates="table_metadata") + + def to_dict(self): + return { + "id": self.id, + "created_by": self.created_by, # 改为created_by + "database_config_id": self.database_config_id, + "table_name": self.table_name, + "table_schema": self.table_schema, + "table_type": self.table_type, + "table_comment": self.table_comment, + "columns_info": self.columns_info, + "primary_keys": self.primary_keys, + # "foreign_keys": self.foreign_keys, + "indexes": self.indexes, + "sample_data": self.sample_data, + "row_count": self.row_count, + "is_enabled_for_qa": self.is_enabled_for_qa, + "qa_description": self.qa_description, + "business_context": self.business_context, + "created_at": self.created_at.isoformat() if self.created_at else None, + "updated_at": self.updated_at.isoformat() if self.updated_at else None, + "last_synced_at": self.last_synced_at.isoformat() if self.last_synced_at else None + } \ No newline at end of file diff --git a/backend/th_agenter/models/user.py b/backend/th_agenter/models/user.py new file mode 100644 index 0000000..c69d632 --- /dev/null +++ b/backend/th_agenter/models/user.py @@ -0,0 +1,92 @@ +"""User model.""" + +from sqlalchemy import Column, String, Boolean, Text +from sqlalchemy.orm import relationship +from typing import List, Optional + +from ..db.base import BaseModel + + +class User(BaseModel): + """User model.""" + + __tablename__ = "users" + + username = Column(String(50), unique=True, index=True, nullable=False) + email = Column(String(100), unique=True, index=True, nullable=False) + hashed_password = Column(String(255), nullable=False) + full_name = Column(String(100), nullable=True) + is_active = Column(Boolean, default=True, nullable=False) + avatar_url = Column(String(255), nullable=True) + bio = Column(Text, nullable=True) + + # 关系 - 只保留角色关系 + roles = relationship("Role", secondary="user_roles", back_populates="users") + + def __repr__(self): + return f"" + + def to_dict(self, include_sensitive=False, include_roles=False): + """Convert to dictionary, optionally excluding sensitive data.""" + data = super().to_dict() + data.update({ + 'username': self.username, + 'email': self.email, + 'full_name': self.full_name, + 'is_active': self.is_active, + 'avatar_url': self.avatar_url, + 'bio': self.bio, + 'is_superuser': self.is_superuser() + }) + + if not include_sensitive: + data.pop('hashed_password', None) + + if include_roles: + data['roles'] = [role.to_dict() for role in self.roles if role.is_active] + + return data + + def has_role(self, role_code: str) -> bool: + """检查用户是否拥有指定角色.""" + try: + return any(role.code == role_code and role.is_active for role in self.roles) + except Exception: + # 如果对象已分离,使用数据库查询 + from sqlalchemy.orm import object_session + from .permission import Role, UserRole + + session = object_session(self) + if session is None: + # 如果没有会话,创建新的会话 + from ..db.database import SessionLocal + session = SessionLocal() + try: + user_role = session.query(UserRole).join(Role).filter( + UserRole.user_id == self.id, + Role.code == role_code, + Role.is_active == True + ).first() + return user_role is not None + finally: + session.close() + else: + user_role = session.query(UserRole).join(Role).filter( + UserRole.user_id == self.id, + Role.code == role_code, + Role.is_active == True + ).first() + return user_role is not None + + def is_superuser(self) -> bool: + """检查用户是否为超级管理员.""" + return self.has_role('SUPER_ADMIN') + + def is_admin_user(self) -> bool: + """检查用户是否为管理员(兼容性方法).""" + return self.is_superuser() + + @property + def is_admin(self) -> bool: + """检查用户是否为管理员(属性方式).""" + return self.is_superuser() \ No newline at end of file diff --git a/backend/th_agenter/models/workflow.py b/backend/th_agenter/models/workflow.py new file mode 100644 index 0000000..a5e3dda --- /dev/null +++ b/backend/th_agenter/models/workflow.py @@ -0,0 +1,175 @@ +"""Workflow models.""" + +from sqlalchemy import Column, String, Text, Boolean, Integer, JSON, ForeignKey, Enum +from sqlalchemy.orm import relationship +from typing import Dict, Any, Optional, List +import enum + +from ..db.base import BaseModel + + +class WorkflowStatus(enum.Enum): + """工作流状态枚举""" + DRAFT = "DRAFT" # 草稿 + PUBLISHED = "PUBLISHED" # 已发布 + ARCHIVED = "ARCHIVED" # 已归档 + + +class NodeType(enum.Enum): + """节点类型枚举""" + START = "start" # 开始节点 + END = "end" # 结束节点 + LLM = "llm" # 大模型节点 + CONDITION = "condition" # 条件分支节点 + LOOP = "loop" # 循环节点 + CODE = "code" # 代码执行节点 + HTTP = "http" # HTTP请求节点 + TOOL = "tool" # 工具节点 + + +class ExecutionStatus(enum.Enum): + """执行状态枚举""" + PENDING = "pending" # 等待执行 + RUNNING = "running" # 执行中 + COMPLETED = "completed" # 执行完成 + FAILED = "failed" # 执行失败 + CANCELLED = "cancelled" # 已取消 + + +class Workflow(BaseModel): + """工作流模型""" + + __tablename__ = "workflows" + + name = Column(String(100), nullable=False, comment="工作流名称") + description = Column(Text, nullable=True, comment="工作流描述") + status = Column(Enum(WorkflowStatus), default=WorkflowStatus.DRAFT, nullable=False, comment="工作流状态") + is_active = Column(Boolean, default=True, nullable=False, comment="是否激活") + + # 工作流定义(JSON格式存储节点和连接信息) + definition = Column(JSON, nullable=False, comment="工作流定义") + + # 版本信息 + version = Column(String(20), default="1.0.0", nullable=False, comment="版本号") + + # 关联用户 + owner_id = Column(Integer, ForeignKey("users.id"), nullable=False, comment="所有者ID") + + # 关系 + executions = relationship("WorkflowExecution", back_populates="workflow", cascade="all, delete-orphan") + + def __repr__(self): + return f"" + + def to_dict(self, include_definition=True): + """转换为字典""" + data = super().to_dict() + data.update({ + 'name': self.name, + 'description': self.description, + 'status': self.status.value, + 'is_active': self.is_active, + 'version': self.version, + 'owner_id': self.owner_id + }) + + if include_definition: + data['definition'] = self.definition + + return data + + +class WorkflowExecution(BaseModel): + """工作流执行记录""" + + __tablename__ = "workflow_executions" + + workflow_id = Column(Integer, ForeignKey("workflows.id"), nullable=False, comment="工作流ID") + status = Column(Enum(ExecutionStatus), default=ExecutionStatus.PENDING, nullable=False, comment="执行状态") + + # 执行输入和输出 + input_data = Column(JSON, nullable=True, comment="输入数据") + output_data = Column(JSON, nullable=True, comment="输出数据") + + # 执行信息 + started_at = Column(String(50), nullable=True, comment="开始时间") + completed_at = Column(String(50), nullable=True, comment="完成时间") + error_message = Column(Text, nullable=True, comment="错误信息") + + # 执行者 + executor_id = Column(Integer, ForeignKey("users.id"), nullable=False, comment="执行者ID") + + # 关系 + workflow = relationship("Workflow", back_populates="executions") + node_executions = relationship("NodeExecution", back_populates="workflow_execution", cascade="all, delete-orphan") + + def __repr__(self): + return f"" + + def to_dict(self, include_nodes=False): + """转换为字典""" + data = super().to_dict() + data.update({ + 'workflow_id': self.workflow_id, + 'status': self.status.value, + 'input_data': self.input_data, + 'output_data': self.output_data, + 'started_at': self.started_at, + 'completed_at': self.completed_at, + 'error_message': self.error_message, + 'executor_id': self.executor_id + }) + + if include_nodes: + data['node_executions'] = [node.to_dict() for node in self.node_executions] + + return data + + +class NodeExecution(BaseModel): + """节点执行记录""" + + __tablename__ = "node_executions" + + workflow_execution_id = Column(Integer, ForeignKey("workflow_executions.id"), nullable=False, comment="工作流执行ID") + node_id = Column(String(50), nullable=False, comment="节点ID") + node_type = Column(Enum(NodeType), nullable=False, comment="节点类型") + node_name = Column(String(100), nullable=False, comment="节点名称") + + # 执行状态和结果 + status = Column(Enum(ExecutionStatus), default=ExecutionStatus.PENDING, nullable=False, comment="执行状态") + input_data = Column(JSON, nullable=True, comment="输入数据") + output_data = Column(JSON, nullable=True, comment="输出数据") + + # 执行时间 + started_at = Column(String(50), nullable=True, comment="开始时间") + completed_at = Column(String(50), nullable=True, comment="完成时间") + duration_ms = Column(Integer, nullable=True, comment="执行时长(毫秒)") + + # 错误信息 + error_message = Column(Text, nullable=True, comment="错误信息") + + # 关系 + workflow_execution = relationship("WorkflowExecution", back_populates="node_executions") + + def __repr__(self): + return f"" + + def to_dict(self): + """转换为字典""" + data = super().to_dict() + data.update({ + 'workflow_execution_id': self.workflow_execution_id, + 'node_id': self.node_id, + 'node_type': self.node_type.value, + 'node_name': self.node_name, + 'status': self.status.value, + 'input_data': self.input_data, + 'output_data': self.output_data, + 'started_at': self.started_at, + 'completed_at': self.completed_at, + 'duration_ms': self.duration_ms, + 'error_message': self.error_message + }) + + return data \ No newline at end of file diff --git a/backend/th_agenter/schemas/__init__.py b/backend/th_agenter/schemas/__init__.py new file mode 100644 index 0000000..ff37910 --- /dev/null +++ b/backend/th_agenter/schemas/__init__.py @@ -0,0 +1,16 @@ +"""Schemas package initialization.""" + +from .user import UserCreate, UserUpdate, UserResponse +from .permission import ( + RoleCreate, RoleUpdate, RoleResponse, + UserRoleAssign +) + +__all__ = [ + # User schemas + "UserCreate", "UserUpdate", "UserResponse", + + # Permission schemas + "RoleCreate", "RoleUpdate", "RoleResponse", + "UserRoleAssign", +] \ No newline at end of file diff --git a/backend/th_agenter/schemas/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/schemas/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d9ef9f49e2849f78dc2a0816c52ef32e6d2528fc GIT binary patch literal 489 zcmZut!AiqG6x>bHBu%a27c>_SIaqH!ND+#Pkb+=CZ-r%TmUJ=MgtzIz-uwtZ!q4&O zUPZ*4n1fe$tGN{4VVF0}n+MbM`vKC^yLemW7@=3Qv}65|W~|90N)Vs~C)O0l7Fe+j zcI4_l!PcvMPmSk)wiHnX}escO z>7uNdm}E@U#|7nLcfHl6Fp+L$qT3PmLjLx(AjC99$gsLs{fNAGC#j7Hyw)SDW@X^sZ=ia_Q=bm%ED^^NMEDStF`?90&+ZpEH@k{Q}%go9hU~VuRLl{oO zXV0k#kaua+O7sVzGg1hCW=>qC#cSvc{s7T2!hmHYyf2ZWGtWxnYJ?pwG=@ zv!Z{3+d}7AJyZiTY**wR+>=ypr}C{Tc_)?EQu#JTZsWSBybj*70oJm9yB23G&w4xB zaXW|u{Ol>Q1pG{LGxU~a6X;^~K(Zfvv^e)(_HVB)zkY4$vyZcXcYEo*36y+#y`y6p ze1lo`0r|EVImu-D+R{gfn>x`l<_~%TLU1HH3Os)?#~1m8h?n@OuZnDt9}||hVHai~ zi|<~`{_MT%jVq!lOhOm@K7r7#r7>h_*U%X{vjVgl&l9j-zv~kt*-b-(LPCe(&QCq&*icVC56e^07d)&?@_ej|X-=77&0h z_IS?6_yF!dd@J*K&iY9t8t?~&ULO8shI}v>iqa?=0eubX zYmuO*lSU*M>q#pRQRfdvi5ofn*+7Vok}aru5=kc#bTaZ35Rn}Yg#zSh{JtB>vq)GZ zc+|uyk0*jJ^?LYdl=z2ZQ32n+2Xrg&$B2h|b`OPMFZYfJ!D#y!?+>p1I?Q{)-F#H+ z;n8eJ4*o-dz?i&Z9pQ0;-b4u=;K&Ks^q(>d?8XIa)wFk(Pjufoow5#0?3;9@*@1=1 z+PG`3X8pW=gOJSKC}gqGG<{ zi;k4*iIN#e8Jr@7fD3#=#u@O zl>1DQt(v|#tDpICnmx17);{M=_~s+YW2aMXo+Mj2JsOYAgwm{Mp=sl6bYAyGL#pWn zeT!?xk!DZ)8D_O_E|{v`GjV7#nr8Pb)YQiZlUom@Y7W9E*))4_p|&9&OKv-qs_mUP zJl&FJdl%ZbCAPzYwGV)xExvtLH_OeMe)U|ceDgoE1LRO{sSEO^Tq`gu$3R_}H}hF@ z2gXf!IJS=2A+ZePPt2%#MQ2dyjGU2}MoezASf<43ZX!V9UPjlIBK71u*-vh-iPv(Z zrmpVQ%CEu?^bZ{C$}fQ2V4j4Es9zJ*4r-3eV*|1B?Bz3v*N7=PnTqcq(|msOK+1%oNQ~_KcEH*zXk$Bcaep zKoIrAyx$jtSRe2Y^TWJojAGOaMn(OlQGYlln#Mw*U}Q8Dg)&DF!Vy6@2fYRY{1`79 zys=0$G$tB+q1Z4VieWgoJAZBQ&0C9aytee~zfp40ZYR}!^XVJw=`LxvI3Y#o1EE(0 zB3i!B2Vz1GAt55NKM47Qpwf8*5m9qNwDAEn@_BNpEfT?HbUK}+7d}OsT$Vx9mUExj zv}U_I9x}nG6*&rIg82)}IxI<3?R^VVTylNq)XuBBlJ)KBqV})K>ywR7CC(?j$z8{i z{exfjpGx(gO1>0G_m3ry1(S|Yx;&gL4rj`&*I$`>V zUQ)aTc2w~RP{~iub8@b=R0Kg11yDn!&n4)BMG2O2`&8-@?!1rotik=p^Q zvb=KfAUYqp0kXwr>;qH71ar5jbn?8kfLEW3zmP6!z=e18q|5gwi}ydgxR?R@+-C9| ze3Ivpd=JT9BrhPrLrMfBK_mx}U?XX7&}Ycu9KoH6;BsHVfd6NG>ExxfzV3n6q_17k zF@_EIk$X_d7|JHc)4*wUlA>+}IjKkN4A(??FoWW=@&c?p5Rh6R}hEQi!be$?)Be#t>M|AEkRsI?bBk)|tT8iec#drR=_*P>5{MAQFz>Jh4 z!3v!~y)Kn@qzbueB($7^`vAG7t3z%B5DHY8a4 z5j@`H2_&@qYe%jF2`vP-B1fyiZOC;Y*^Y!3hj-?(PnRM-*@0@of_Y5QlPkuRgIyHDY=?SL?&nE{+vMG|T zjwY?qOhx6}J7;$Oa#ymr9^&n!iN>{p1y&N+&)~gufnl@%@6#B15I#J3>f=*m3+Q1v zN5Po<0Ld5-bylLmUzLc~uBM!b`mq^0n!K3ra&3JxlwMqsNFI9(ryR6h#i8 z_E8|}uStdHu>&O7apWVYOeE}6*Eq{2Y!X_i^8om1o9>!wN3cLKO#hv2N$zy2!IK9v z9tva<=kIXI@B30tArE8(#4Fnqd+$7xavTLHbYj{&^>Ug$`oDlfl?fL`S*}xgICP43 zKi3UF?d6n%%flfK;ZSs*y;G4qekSGSRd9$$IONMAr8YoH3LHA~C^+Np`e+LG|N2(8UcpP7atbAO)frw zdtg@=AFZgM#Yd~;dAM^0#fJd|_^iyqR<2)s3>i2bO-n}LScTIooB=rmPeX78Nt2~P zDoa3SQOV4*T@lFivYkbi6@#oqmeGO*+Lo$h#j;(QN=C~Tu(PUUrLvt3WVx}Fp=_ue zMlTgI$k`NGx!j@xT41qIrMkc~a8hwo4K3&;D7>u(?5gFFR>^JcAj{3DT9(zSWV8T+ zwsj!OjmnO8^isMOE~V>sV2KP5*)UWODc%m3q;QeCOUF4>Dc->~kcJVXyHT_$CG&A1 z5(fAsET4eZ>s}}R@>=%xn=(ihwSJ!zT4*N6ieopWeI3u_qeOB!7QRs|_>p30KkFajNXn$z(9n!Vx zas(O<(DJ?SreVDRE0&!lp)u7Z(Ru~HXpp)iuR(X%<%$u=6(&>HaJ5W2NU!6I+*(lw zmw=*y54^&UN4z=^<{tDA{J!!&5crKm1GpYI9&R)S;f5d8O8Np=l9L5>sx+xn>OAf4 zH=!+*SA#kZ?g3Z^ZUfl$E!!;&(+=I2qOG1|(6&q0@(6phY+*(iw?TG;Vi?#m=5JB6 zdHKJ-`|dlphMa@1ZZ;3+L<2y$m=FJKw@j`(o=e0)>9cDC#H zt~u}T+7kUYx1?MCvW9>!mVvgVmkJQ5{p*c5WD!P0iV=1|OC{t>?d?aJ7xO_C@Xv!zdrc6U~KHoNH zoGtkr+mdl@%BRFl*iN|?Y|6OXRk^P{)3!yG@3v%`TJyJltzVbnYps=OwlY&*oo1^u z?VS%@<)zuqjMbiI?U`0L9fLh(ZBDc9HA5;GAUFgV_e~#+b2CTMY}BkQ zBWAZn^()SR5Q#4T1Z>s|yNiFidhgZ^IqY6)I4Hoq)D~xS)E{xeKi&x!)KTY{5Q)H5 zbBF&PcvuY2p4{voCl_D4MPZI;l#QgY<}P})=VFv15Gdpu_HN{8-MF4t;hiKwf)j|i zfJl{`qd8-0%T&~4Dw`h|*#<+-pZn|>3(7(Ql$mLS{ zLnq`-tRjl&W*E3!6@e8)PDZN;_yNwqscVN^rlqw*{zqXY)0S~%#0uHeD%L1rsypC^ zn@uIBUWNI@u-;z-nX@0izc~3%OTW97RVCMNKAl{8a}F-XvQyVU_06YK9pnnl*ut^rQsUdti<}b4W#HFF7aD+m1qw7ISCBxCk^>V( zDz-;^Tn}1O7wbljPE3c~ng~flB84IJb?f=#Ti6TqfbPK=If}Mx#?f zz@l+v72}9BPkd#c+e%})7)GSuylBj(VZ8`thmlZp zM8h>bU35x?!CW*wEB(aw9Qh5jz)gRyVh3TBrT141BEiX*Ovpq}a~NZt>b?DH`WzbL=*Yle$?LcO4>xkR?VTCQHtx zl+=Cb+!ak_q@LY0Sq)^^oNY+#ZbOZsz&vL|^4LcU0%n`&{?JIa-df1TUU$wVsh`a0 z^Ddx|hv(dTd3ovb{E-2H`1KXEe5=Xhf_t09!{f^drw8N!IHXgS3WScqkS zBU}*gxl{arl~@OC#5Q0jcB?v5hvB!LF))RD_R%T zIzj8I>FcIiH)uUIeLYm`1#Lr3UvJze_}^$bn@Y&RGh?wxGOa{|YV?vk5>dNL_;A6Z zIbIn}E2(58q4B2`H7#(O^LRv+Urxp3gl2t7krOe^c_oriVv%%;2(0D+K~5_pa#UZw zj&|dw{1FJZm@q?F*dL3?60VC8ErbtS;mtXb7p)P$Eo=`vM8`Lvf5qk$UDW2J zHh0D55xruA=sUy_SJ)l4<3Ye!f5qM?Hi}JRGuS;~_Zs$X75jFvMci@7LK?z87#Bdj z*h>6ykI<$$aDVWXOAb(HngKTM-PoA;W8qgz>vzVBb9V}VndgHG}nldloE0>p1uTf$7iZd=xf(u(SzJ zshPhxHMQ|zVctR-VGwDmfCcBU0Th3Bqp&nJZ_zAqnc6j%nj&c_MPf2JM`tR|d|a5m zTYNBH{N(;Tt9NoLsuYQ)l`HZ*BQ$CbNy4Q`lID`6kyI>`K-nWnmot%sVev`QMTMy8 zgp!n#DKOh5DVB;#62Wy4+%}C<(?o-gkz_KJj-XqqU~fS?T{}8FX{SAvWSRs~;)heI zgtlFhR2&hNBIz_yhBIjyhlZd>8Gej*^2MHTYE(}4#^q#s|42khK7KnKiT+ql#;96~ zV7IX0>)}zEj3}xK>**dHBN*Tm@VwO|z`M*E-@fMGJ{_HnEc89Rkn^7#@6UGR`E#c7 z_`|`RU(}W2T4>*FdXf8dcP{kW_^Ii@Ovn4}dH%JvVApJ9u6OZ;T(Ez9VA?U${JuBO z_pi0=oavuS=30)9pUkHJ#vdi$G8a%6K}I%U%HIKVeFRnG=OB6^G?8y(4(cL9Y@(Ie z<2J#rxzLyUhzt>0LYJZ5T*%%o++Wgt+VEz2CQyXN@PB9q@|T z?w~PhOFLI%H{Yjb0&~Sr8M>I8Va}Ub72e5wJDG*&+iM0IqB*{knYRvVtfVm-yFc?? zu@ZxwGXfDFQ;#p{LajrFAmg&Xf}JoqIj z*MEO^p4aR|9!*4|vSwFQaQe)M=6W@f$jC1ek|LTlGYU=wamGaxs>WVXaTkI?vL7y* z*GP}Dgh8wYs)x{BXW0gw)n0&c=3hSl)Y!z>O!Lfe-WOW&2|1rI*D*)(zUNkaJvm>` zitk9ycjU9CUCZqk@=emRM_Pp;S0}DsA6s?=bQcQ2RD*(p?1d}oKmza5MICu3;Sf+uD%5lDD>eY2mD`Wsb4!D5Ing#4%C$RhoH9G7?DhF^PWdX?XzyKAy4|BP}AAV#3$^mwR1Ds-WjeVQAopOLZ z>@#L>p;kXIliTz?aYs#LHHrb!6z7CiJ+g*Ik^4$J;gK}5inl*0{(PY>vIYsV1Mwm> zAovh4I}$v8vK@hD#2qLF5Lyvvf<&((Z3y@bnkva|l=dJ50k#g>C9q)74u6mNKL}kj zyd!?P8q-B!jb^I`@(|m}5Tr!%Ji=js+SsDrt4EZ+`SlTnPOqK@fQWK?CXUWF-fO+n zI(J~fk`EnN2_4Rb4lnjSY|4lFS3;+Aq0=j&!CYvtHmd%yt37+!jH}MsXO|tF)!-7E zpB!5i)+3nc2s#qM7q6i^WI7@s1QDJ_z|DAaNMdpz2zdPJ4FH{*+V;%y3yq8F@|g>{ zHVJ6xsp$g~`}4fCW?0ClZ|2%AR1FuthNEJ0mlrSPf+wmRbpoj?w%CBmliAm2_=z|3 z{E6B`&{atUUHbOmBI^oGt+{rQ-F>O#MI~Osa5+cQ_n=_g$|W`5MUon+shze;_?k_hj=TcX;gqT!;2N{q zw)?tIKh?9KpwAI}TRlc+73$H{)g9P<0D;E9DU>i8$;$|IvaN{cCA1>o8K^@5)XP^L z?zc~@`U81Bu-YY1?+oPpd-J@o8tkk(YiIr47d9rew`vXjhi9#OHW>i@mGC)rCxL#2 zt(*X;KZ(Z~3)n*cN}-Nvu#8=_g5DVhmZC0de#5wdMI03tf$ugm$q|p}BHp-L@MsM~ z;2n6vy-|om45BqwYA|zav|#-)R5``$fi;tggfz|+eSXRphw@E5%buRk z+zr{w*Pp*B{-Sy22e(^3IG=Y1sr`7qsc+fS2US|uQDu-pq2bAagr`DYMW}|<5Ahar zsm>JVQQ9ho&Y=MTUtjG5z!>USYYxmb&pn-MJ~)0NE9UuwND$oev%R^tDj(O(qO#7zI z#vaR#Z8C7#Y_hYh(}7I}E}PptY}a(_CIgqv-5z$&^b08O+1%}B8>fHt6$2OgM*js2 C144}e literal 0 HcmV?d00001 diff --git a/backend/th_agenter/schemas/__pycache__/user.cpython-313.pyc b/backend/th_agenter/schemas/__pycache__/user.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..63c39e4a4e71e35d89c4651f07d2a83ddde5a967 GIT binary patch literal 3964 zcmb7H>u(#!5x?Ud$>U4Z)0HhtqD`qbYnctA$c~*yk#2rDtaMd&JCEjs~NNg@%^^N<855{iUbJaf{Ab|+i$(`n8?+H$p65b)f zUY`UR*9TmGB3R>w7&idiV1sTGH30|$r`Fo#fgk*QE?=bw5V{!Sd6)-?`750u4Pl2PTgY} zO;5#mDy39QvoiBq#f2;IG>A%5nz@iQOf6||Y15iLe+={6T!JG!*wiKSMCj*a7ZDR~ zu#O;$Xt7HI@g%$nN%o!rZAT)>zDq9RV|;&&ACPUnA8zS&qqZamQ7Q*Wuz?qHc%cSf z6XS(RQ#uf9rV`o&&tE}QEVVEb&A{BTQ#@Wlr&XQ?a?eHm^BF_YH6v}!!B~B(I+k*< zjG=-}QmDw2Ht47u44u&@sc$x?>k5jgFq6qrzxt+XsYJ;U{T>$!Q%PY$MWKF0na`$j zx(4H*qP&q)b^HuwVMUqE5Yy5#hGt}e;Z>AWHmN8?f~`n9?kAWQK}FRtvX+WoH9`Ib zly@S*n~>v3P5_}DiejQ#Qc*37WM*=fhWcF~+k+oRhet0bvJ0AVC9N6OdAN&lcsipd zzo{81##U5ROE~ySphe;fizEaqF@gp*zYgSkTuJOIwI5%L-aU2SRcL>1`IVJqQGBk{ zax8DIn}wEh%cCoUMe$szw{QK%MzYX*dHL0qx7LK!ZxzMM+sC_?hgUlD(bevv*u5R; zTOQ9#Yi+ke55+#xSKaIY3=tvN_Rx!uharH?2BC++#7lf>DdwYo#Kkbt0QnVk?NSE= zN)kC><~mpq@!`zepfFWw}xCxTS8=@YMEWUh4M5Q02(vj9< zQ*dGvBC{q9Fk=c3n{}QYuL21*@L0S$k>4bT!5NVAYO6FmtAv5q?CjgZc&(%*EyUVv zH%%{Kf~p|mm)e+{a*R`T(j*Ckh%_TOJBs$!>|-r zzg-JGJlD~Pn*hn<5qB{3929sNnuNv|0CK!P9gKx&$MBqLq_wX)Q8uN$k<(18f^oCd zYs(_l5R4F8S_MITsd+d@2qdhU3cO^2lx8MLhCPV1IcX~>)ioFj{vDjuZq~lJ=VGBw zo`F4S(9mwyl`(QBV}U0iV+gbwV*Xj2;sp)|K4vb01cfHz1icY+&aBS%Spqi}`6gy!)DU z*aa9aOMO>T@LGc5L7`@_|MG(!S{L90auEr$fNi@c-&%p|DHd_!0^K_gJT z$o>-?LI4{H0RfRFZWHFY*i|wp^SkC|Y(ftRX{_)-2 z2fx|<;LdLTz4EO!sN%dSZC1_cmIG5rUS@7VBRSaQz-4(@2HPI4ckr@UtjPj#r^T3? zvaDJ?UVJ!f%x2P+LHOo|^U`^aLOl)7lI(m#t#K64smET9ybR0XlBO5Pd))Skp7+Bx zjz@u!bZiJijzcpG@Z0+Y$Zc+#pMw5yn?j94t!jD0^;*@>@U_fWJ#^O5S0SyvhUZ`n z*2m}iP#bcLSNjHDXwdW>QiExA*J-GO5jWHL55e^mH|-kXFYtX_Ovrr&*pEK`;q|F& zbbm#7!@-N>1$Dh8x^SfW}-ua;XtM7uJ8(q5dvyXQE zaGSgYYuK-o8w<=7RP|6biplK4BK6LxCf0mZN~*f9&gdFbj|o&r>LwMkGvA;hyrQa! zR*OU;q#t@JF$ZSJJZ0ri4FOd)MzM*;U*G@GJBs8`jr9WZkYL=I zF9KmT)^@nS91Q_k% z$UZtQI0=P91bXVR78f!`+CCcoNh63qg5?=hjf*MOfU2Az;_Q1MifAZjWpp#{)Yl|{ z(hw2^5vvH;1HsZAJO81JQR#X6zr~B>27G~dH8mig2|Ul2xVBwxpvVpUjqCp_*Ink$ z?t46Z|H|S%2i<ALhIA O&#!m>lY;>>!hZmZ>5RYt literal 0 HcmV?d00001 diff --git a/backend/th_agenter/schemas/__pycache__/workflow.cpython-313.pyc b/backend/th_agenter/schemas/__pycache__/workflow.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07e6b572bf42d3ebf5a5f37842c0a21e2e276491 GIT binary patch literal 11812 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 CI_#AI literal 0 HcmV?d00001 diff --git a/backend/th_agenter/schemas/llm_config.py b/backend/th_agenter/schemas/llm_config.py new file mode 100644 index 0000000..165cf78 --- /dev/null +++ b/backend/th_agenter/schemas/llm_config.py @@ -0,0 +1,152 @@ +"""LLM Configuration Pydantic schemas.""" + +from typing import Optional, Dict, Any +from pydantic import BaseModel, Field, validator +from datetime import datetime + + +class LLMConfigBase(BaseModel): + """大模型配置基础模式.""" + name: str = Field(..., min_length=1, max_length=100, description="配置名称") + provider: str = Field(..., min_length=1, max_length=50, description="服务商") + model_name: str = Field(..., min_length=1, max_length=100, description="模型名称") + api_key: str = Field(..., min_length=1, description="API密钥") + base_url: Optional[str] = Field(None, description="API基础URL") + max_tokens: Optional[int] = Field(4096, ge=1, le=32000, description="最大令牌数") + temperature: Optional[float] = Field(0.7, ge=0.0, le=2.0, description="温度参数") + top_p: Optional[float] = Field(1.0, ge=0.0, le=1.0, description="Top-p参数") + frequency_penalty: Optional[float] = Field(0.0, ge=-2.0, le=2.0, description="频率惩罚") + presence_penalty: Optional[float] = Field(0.0, ge=-2.0, le=2.0, description="存在惩罚") + description: Optional[str] = Field(None, max_length=500, description="配置描述") + + is_active: bool = Field(True, description="是否激活") + is_default: bool = Field(False, description="是否为默认配置") + is_embedding: bool = Field(False, description="是否为嵌入模型") + extra_config: Optional[Dict[str, Any]] = Field(None, description="额外配置") + + +class LLMConfigCreate(LLMConfigBase): + """创建大模型配置模式.""" + + @validator('provider') + def validate_provider(cls, v): + allowed_providers = [ + 'openai', 'azure', 'anthropic', 'google', 'baidu', + 'alibaba', 'tencent', 'zhipu', 'moonshot', 'deepseek', + 'ollama', 'custom', "doubao" + ] + if v.lower() not in allowed_providers: + raise ValueError(f'不支持的服务商: {v},支持的服务商: {", ".join(allowed_providers)}') + return v.lower() + + @validator('api_key') + def validate_api_key(cls, v): + if len(v.strip()) < 10: + raise ValueError('API密钥长度不能少于10个字符') + return v.strip() + + +class LLMConfigUpdate(BaseModel): + """更新大模型配置模式.""" + name: Optional[str] = Field(None, min_length=1, max_length=100, description="配置名称") + provider: Optional[str] = Field(None, min_length=1, max_length=50, description="服务商") + model_name: Optional[str] = Field(None, min_length=1, max_length=100, description="模型名称") + api_key: Optional[str] = Field(None, min_length=1, description="API密钥") + base_url: Optional[str] = Field(None, description="API基础URL") + max_tokens: Optional[int] = Field(None, ge=1, le=32000, description="最大令牌数") + temperature: Optional[float] = Field(None, ge=0.0, le=2.0, description="温度参数") + top_p: Optional[float] = Field(None, ge=0.0, le=1.0, description="Top-p参数") + frequency_penalty: Optional[float] = Field(None, ge=-2.0, le=2.0, description="频率惩罚") + presence_penalty: Optional[float] = Field(None, ge=-2.0, le=2.0, description="存在惩罚") + description: Optional[str] = Field(None, max_length=500, description="配置描述") + + is_active: Optional[bool] = Field(None, description="是否激活") + is_default: Optional[bool] = Field(None, description="是否为默认配置") + is_embedding: Optional[bool] = Field(None, description="是否为嵌入模型") + extra_config: Optional[Dict[str, Any]] = Field(None, description="额外配置") + + @validator('provider') + def validate_provider(cls, v): + if v is not None: + allowed_providers = [ + 'openai', 'azure', 'anthropic', 'google', 'baidu', + 'alibaba', 'tencent', 'zhipu', 'moonshot', 'deepseek', + 'ollama', 'custom',"doubao" + ] + if v.lower() not in allowed_providers: + raise ValueError(f'不支持的服务商: {v},支持的服务商: {", ".join(allowed_providers)}') + return v.lower() + return v + + @validator('api_key') + def validate_api_key(cls, v): + if v is not None and len(v.strip()) < 10: + raise ValueError('API密钥长度不能少于10个字符') + return v.strip() if v else v + + +class LLMConfigResponse(BaseModel): + """大模型配置响应模式.""" + id: int + name: str + provider: str + model_name: str + api_key: Optional[str] = None # 完整的API密钥(仅在include_sensitive=True时返回) + base_url: Optional[str] = None + max_tokens: Optional[int] = None + temperature: Optional[float] = None + top_p: Optional[float] = None + frequency_penalty: Optional[float] = None + presence_penalty: Optional[float] = None + description: Optional[str] = None + + is_active: bool + is_default: bool + is_embedding: bool + extra_config: Optional[Dict[str, Any]] = None + created_at: datetime + updated_at: Optional[datetime] = None + created_by: Optional[int] = None + updated_by: Optional[int] = None + + # 敏感信息处理 + api_key_masked: Optional[str] = None + + class Config: + from_attributes = True + + @validator('api_key_masked', pre=True, always=True) + def mask_api_key(cls, v, values): + # 在响应中隐藏API密钥,只显示前4位和后4位 + if 'api_key' in values and values['api_key']: + key = values['api_key'] + if len(key) > 8: + return f"{key[:4]}{'*' * (len(key) - 8)}{key[-4:]}" + else: + return '*' * len(key) + return None + + +class LLMConfigTest(BaseModel): + """大模型配置测试模式.""" + message: Optional[str] = Field( + "Hello, this is a test message.", + max_length=1000, + description="测试消息" + ) + + +class LLMConfigClientResponse(BaseModel): + """大模型配置客户端响应模式(用于前端).""" + id: int + name: str + provider: str + model_name: str + max_tokens: Optional[int] = None + temperature: Optional[float] = None + top_p: Optional[float] = None + is_active: bool + description: Optional[str] = None + + class Config: + from_attributes = True \ No newline at end of file diff --git a/backend/th_agenter/schemas/permission.py b/backend/th_agenter/schemas/permission.py new file mode 100644 index 0000000..553999b --- /dev/null +++ b/backend/th_agenter/schemas/permission.py @@ -0,0 +1,68 @@ +"""Role Pydantic schemas.""" + +from typing import Optional, List +from pydantic import BaseModel, Field, validator +from datetime import datetime + + +class RoleBase(BaseModel): + """角色基础模式.""" + name: str = Field(..., min_length=1, max_length=100, description="角色名称") + code: str = Field(..., min_length=1, max_length=50, description="角色代码") + description: Optional[str] = Field(None, max_length=500, description="角色描述") + sort_order: Optional[int] = Field(0, ge=0, description="排序") + is_active: bool = Field(True, description="是否激活") + + +class RoleCreate(RoleBase): + """创建角色模式.""" + + @validator('code') + def validate_code(cls, v): + if not v.replace('_', '').replace('-', '').isalnum(): + raise ValueError('角色代码只能包含字母、数字、下划线和连字符') + return v.upper() + + +class RoleUpdate(BaseModel): + """更新角色模式.""" + name: Optional[str] = Field(None, min_length=1, max_length=100, description="角色名称") + code: Optional[str] = Field(None, min_length=1, max_length=50, description="角色代码") + description: Optional[str] = Field(None, max_length=500, description="角色描述") + sort_order: Optional[int] = Field(None, ge=0, description="排序") + is_active: Optional[bool] = Field(None, description="是否激活") + + @validator('code') + def validate_code(cls, v): + if v is not None and not v.replace('_', '').replace('-', '').isalnum(): + raise ValueError('角色代码只能包含字母、数字、下划线和连字符') + return v.upper() if v else v + + +class RoleResponse(RoleBase): + """角色响应模式.""" + id: int + created_at: datetime + updated_at: Optional[datetime] = None + created_by: Optional[int] = None + updated_by: Optional[int] = None + + # 关联信息 + user_count: Optional[int] = 0 + + class Config: + from_attributes = True + + +class UserRoleAssign(BaseModel): + """用户角色分配模式.""" + user_id: int = Field(..., description="用户ID") + role_ids: List[int] = Field(..., description="角色ID列表") + + @validator('role_ids') + def validate_role_ids(cls, v): + if not v: + raise ValueError('角色ID列表不能为空') + if len(v) != len(set(v)): + raise ValueError('角色ID列表不能包含重复项') + return v \ No newline at end of file diff --git a/backend/th_agenter/schemas/user.py b/backend/th_agenter/schemas/user.py new file mode 100644 index 0000000..7057e8d --- /dev/null +++ b/backend/th_agenter/schemas/user.py @@ -0,0 +1,61 @@ +"""User schemas.""" + +from typing import Optional, List, Dict, Any +from pydantic import BaseModel, Field +from datetime import datetime + +from ..utils.schemas import BaseResponse + + +class UserBase(BaseModel): + """User base schema.""" + username: str = Field(..., min_length=3, max_length=50) + email: str = Field(..., max_length=100) + full_name: Optional[str] = Field(None, max_length=100) + bio: Optional[str] = None + avatar_url: Optional[str] = None + + +class UserCreate(UserBase): + """User creation schema.""" + password: str = Field(..., min_length=6) + + +class UserUpdate(BaseModel): + """User update schema.""" + username: Optional[str] = Field(None, min_length=3, max_length=50) + email: Optional[str] = Field(None, max_length=100) + full_name: Optional[str] = Field(None, max_length=100) + bio: Optional[str] = None + avatar_url: Optional[str] = None + password: Optional[str] = Field(None, min_length=6) + is_active: Optional[bool] = None + + +class ChangePasswordRequest(BaseModel): + """Change password request schema.""" + current_password: str = Field(..., description="Current password") + new_password: str = Field(..., min_length=6, description="New password") + + +class ResetPasswordRequest(BaseModel): + """Admin reset password request schema.""" + new_password: str = Field(..., min_length=6, description="New password") + + +class UserResponse(BaseResponse, UserBase): + """User response schema.""" + is_active: bool + is_superuser: Optional[bool] = Field(default=False, description="是否为超级管理员") + + class Config: + from_attributes = True + + @classmethod + def from_orm(cls, obj): + """从ORM对象创建响应模型,正确处理is_superuser方法""" + data = obj.__dict__.copy() + # 调用is_superuser方法获取布尔值 + if hasattr(obj, 'is_superuser') and callable(obj.is_superuser): + data['is_superuser'] = obj.is_superuser() + return cls(**data) \ No newline at end of file diff --git a/backend/th_agenter/schemas/workflow.py b/backend/th_agenter/schemas/workflow.py new file mode 100644 index 0000000..46d96cc --- /dev/null +++ b/backend/th_agenter/schemas/workflow.py @@ -0,0 +1,231 @@ +"""Workflow schemas.""" + +from pydantic import BaseModel, Field +from typing import Dict, Any, Optional, List +from datetime import datetime +from enum import Enum + + +class WorkflowStatus(str, Enum): + """工作流状态枚举""" + DRAFT = "DRAFT" + PUBLISHED = "PUBLISHED" + ARCHIVED = "ARCHIVED" + + +class NodeType(str, Enum): + """节点类型""" + START = "start" + END = "end" + LLM = "llm" + CONDITION = "condition" + LOOP = "loop" + CODE = "code" + HTTP = "http" + TOOL = "tool" + + +class ExecutionStatus(str, Enum): + """执行状态""" + PENDING = "pending" + RUNNING = "running" + COMPLETED = "completed" + FAILED = "failed" + CANCELLED = "cancelled" + + +# 节点定义相关模式 +class NodePosition(BaseModel): + """节点位置""" + x: float + y: float + + +# 参数定义相关模式 +class ParameterType(str, Enum): + """参数类型""" + STRING = "string" + NUMBER = "number" + BOOLEAN = "boolean" + OBJECT = "object" + ARRAY = "array" + + +class NodeParameter(BaseModel): + """节点参数定义""" + name: str = Field(..., min_length=1, max_length=50) + type: ParameterType + description: Optional[str] = None + required: bool = True + default_value: Optional[Any] = None + source: Optional[str] = None # 参数来源:'input'(用户输入), 'node'(其他节点输出), 'variable'(变量引用) + source_node_id: Optional[str] = None # 来源节点ID(当source为'node'时) + source_field: Optional[str] = None # 来源字段名 + variable_name: Optional[str] = None # 变量名称(用于结束节点的输出参数) + + +class NodeInputOutput(BaseModel): + """节点输入输出定义""" + inputs: List[NodeParameter] = [] + outputs: List[NodeParameter] = [] + + +class NodeConfig(BaseModel): + """节点配置基类""" + pass + + +class LLMNodeConfig(NodeConfig): + """LLM节点配置""" + model_id: Optional[int] = None # 大模型配置ID + model_name: Optional[str] = None # 模型名称(兼容前端) + temperature: float = Field(default=0.7, ge=0, le=2) + max_tokens: Optional[int] = Field(default=None, gt=0) + prompt: str = Field(..., min_length=1) + enable_variable_substitution: bool = True # 是否启用变量替换 + + +class ConditionNodeConfig(NodeConfig): + """条件节点配置""" + condition: str = Field(..., min_length=1) + + +class LoopNodeConfig(NodeConfig): + """循环节点配置""" + loop_type: str = Field(..., pattern="^(count|while|foreach)$") + count: Optional[int] = Field(None, description="循环次数(当loop_type为count时)") + condition: Optional[str] = Field(None, description="循环条件(当loop_type为while时)") + iterable: Optional[str] = Field(None, description="可迭代对象(当loop_type为foreach时)") + + +class CodeNodeConfig(NodeConfig): + """代码执行节点配置""" + language: str = Field(..., pattern="^(python|javascript)$") + code: str = Field(..., min_length=1) + + +class HttpNodeConfig(NodeConfig): + """HTTP请求节点配置""" + method: str = Field(..., pattern="^(GET|POST|PUT|DELETE|PATCH)$") + url: str = Field(..., min_length=1) + headers: Optional[Dict[str, str]] = None + body: Optional[str] = None + + +class ToolNodeConfig(NodeConfig): + """工具节点配置""" + tool_type: str + parameters: Optional[Dict[str, Any]] = None + + +class WorkflowNode(BaseModel): + """工作流节点""" + id: str + type: NodeType + name: str + description: Optional[str] = None + position: NodePosition + config: Optional[Dict[str, Any]] = None + parameters: Optional[NodeInputOutput] = None # 节点输入输出参数定义 + + +class WorkflowConnection(BaseModel): + """工作流连接""" + id: str + from_node: str = Field(..., alias="from") + to_node: str = Field(..., alias="to") + from_point: str = Field(default="output") + to_point: str = Field(default="input") + + +class WorkflowDefinition(BaseModel): + """工作流定义""" + nodes: List[WorkflowNode] + connections: List[WorkflowConnection] + + +# 工作流CRUD模式 +class WorkflowCreate(BaseModel): + """创建工作流""" + name: str = Field(..., min_length=1, max_length=100) + description: Optional[str] = None + definition: WorkflowDefinition + status: WorkflowStatus = WorkflowStatus.DRAFT + + +class WorkflowUpdate(BaseModel): + """更新工作流""" + name: Optional[str] = Field(None, min_length=1, max_length=100) + description: Optional[str] = None + definition: Optional[WorkflowDefinition] = None + status: Optional[WorkflowStatus] = None + is_active: Optional[bool] = None + + +class WorkflowResponse(BaseModel): + """工作流响应""" + id: int + name: str + description: Optional[str] + status: WorkflowStatus + is_active: bool + version: str + owner_id: int + definition: Optional[WorkflowDefinition] = None + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + + +# 工作流执行相关模式 +class WorkflowExecuteRequest(BaseModel): + """工作流执行请求""" + input_data: Optional[Dict[str, Any]] = None + + +class NodeExecutionResponse(BaseModel): + """节点执行响应""" + id: int + node_id: str + node_type: NodeType + node_name: str + status: ExecutionStatus + input_data: Optional[Dict[str, Any]] + output_data: Optional[Dict[str, Any]] + started_at: Optional[str] + completed_at: Optional[str] + duration_ms: Optional[int] + error_message: Optional[str] + + class Config: + from_attributes = True + + +class WorkflowExecutionResponse(BaseModel): + """工作流执行响应""" + id: int + workflow_id: int + status: ExecutionStatus + input_data: Optional[Dict[str, Any]] + output_data: Optional[Dict[str, Any]] + started_at: Optional[str] + completed_at: Optional[str] + error_message: Optional[str] + executor_id: int + node_executions: Optional[List[NodeExecutionResponse]] = None + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + + +# 工作流列表响应 +class WorkflowListResponse(BaseModel): + """工作流列表响应""" + workflows: List[WorkflowResponse] + total: int + page: int + size: int \ No newline at end of file diff --git a/backend/th_agenter/scripts/__pycache__/init_system.cpython-313.pyc b/backend/th_agenter/scripts/__pycache__/init_system.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..22040b24efed27778a0f9921e644f2a04fdc7661 GIT binary patch literal 5232 zcmbVQYitwQ6~6Y2?aBC+m-B?=CIqMhF$*E(y(mtCXiU*IvuUXk$(`5}$HE?`cgBUd ze}dE%8(@{7s-mh|^`Vu@s{MfkRr#B2|5U2fM3{!1CTf?Zs`g(>kc!|>&zqy;jJ*n5=VTaHXChzH$>+lk zjeSp4r@~InpCFQ`N^wa}rDaKDRq3({!#+h)6quD2FG>X7IYm;{w47343L6y_Noy!g zOp2;F0gTcLRMunxU7ZHq;)Ygn$_JjK8#+EEsqsuMm6AxJ^g;T=13d%3%_iMU#;mas z%H7rkhk1M$wV^TA93C-^&1ArHq{{YEBFA)dr7x9?NpK!>VDttJn6d^bR|gskl$J&w zB<@8>w$HCI$I4HE9kWbx6q#!UP_NUZF_Mtrl2JdJbWEaCOglWo?n3K;es5UJWUzVO z7;YjXDUs43T-Pf@kqDeUJCT-?g|5NU&6TEM<3f{I$tMzU`;+-hW~R_ML`aSpV{}1d zi{townAhCK{b_7kp3D(2Po!~KNfoI+8eg&xP>|4L*4!l%s~WnTkfy0gYA%T`t}%*A zJ^{Os6bg5&4G$e3&rM5me+mq^Co87q?XMGJ;-Vxcb#@#qT~^1H1W8Y;%DBzoBGWUL zeH(g{R@k5keu@O$EPBH7S7TRV%hf$aHnQP#xdLmPZ{9!WU*+%$hd-YBb?%qBWlDc_ zjSI|s=e*1H#~*Shp09c9H<6Qy2j+cqzU9WqZpkI*aG)&%AlBcoAN z_9L<;4LUM*tj7+YZ6tW;ADV zf;5AqLftXOn6%Joe!9Qz-)SfyyMRZ3%1!8OC_7PPPd;;_dVIU@{?MPeKL`I9T&cS- zd+xKU(Dk<2=myKX>Yi{-KdvouJ1o2nKIDcr0;tM6&&_ck@z0-mkuS7~7#sKw-xKcF z-2LXAv&*4VMef~498aNo!>^;z1ClQ{cx!=rwI?f-PpX zA%UfT<>ZG(vHj>Pr8!CX+DRuy?FhqPiZ$ZTwm7$M8D)7(T-@)@-P zj+ztnD=i?!%Sw_kEO-=t$`$Brm^xTw4{bO-uF#r4G#{Ufull=I{9Q%=p4(lILe00v zZ;pT5^^3xT&O;AEy&s-g^VQDB=3>iDM~l8=kLnu#?LmRPtNuNI@$Y&0jMIVTgr&@= z)_ZT@zT@thJ4cs8CyU&vwP3v-)j}_n!=S~jwkSfsJJ^4KS=_?`xp;u@Kjm3G#!&hc z?UxWA?PHesj_5ng(mO7o6EM$eR##8%`q!mL>?MG75)DiT`rn4x~FWCZ-m(10k*Io0$wSj|}uDuUn3 zNDvFLj%YlS%M)vi%1@?Kd7^I_`4W6AD)b0O>9%#3*M$9!{2`dbKmO?l`0b{((rFt% z|KWA~ttnB(mqev}0N~*A`(YfS`s3MOVapF1sWXNY-#m$Od71CBO^_xg$mK@6KWHshNl?dnMRSef_z)bIZ-&Df+}UUvU2HwX+M~ zxDf-9s)ifE(&LF)*9;W7!8Ojm9I%M}MXpEZ=)Rv09WHWxI>#dR6}kO7C-IO=K7T^X zBF0s31(3dbiTfRQC+}QX4!v9CPE*P1a<&$r+1eThncX#Rp@!ycFE=1vtAC-;6!TW} z_&_cCw3Z({%zV0|KQMTJ`NIJhyl7lJPR}GB4|i&wcs!X)#N&jTuI9`snr9mRwUJ3r z5Ssf?*+~fA@wny&YXe);yrt?fo=g*srNWv!EkoL>8aXhpd^aOz0Jqs0!om`IEY|{M zZX`#tdNQpw&@$C1KqHA9k)WfDFf$C$+6l&?fZA z$mh9}uxdUdy-NmxM{QBL2HghZa5z3k2fsl4Us2UMYWkNO1?v`$UYAz`yH*0bRs-ED zf$mxFBg9^M96-iYhG2#whIB?vrYgbU)S|M?KTXD4?D6p_`N{F|D;Yk6^xg7swTj6vNX>IhPt7aQ%}vbAL+2&|b*JW~fQ93M z#^ja66hlppiI30B%PfhH*DI*J#bJ}1pHiBWYFEStG#KQnVvvhIFf%eT-eM3hVgYgh DHF-Cj literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/__pycache__/agent_config.cpython-313.pyc b/backend/th_agenter/services/__pycache__/agent_config.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2e79e3ac3a9b993659514c9ec0806d20ff9af5a8 GIT binary patch literal 11105 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 bbv?6C&X%8^|A6@)OU7XE|2KuTdO!aM@R)z% literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/__pycache__/chat.cpython-313.pyc b/backend/th_agenter/services/__pycache__/chat.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..d92f032df7f80c9cad6324bb9e786b682a9e4100 GIT binary patch literal 13994 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 KyGt;nBL53zCdXF* literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/__pycache__/conversation.cpython-313.pyc b/backend/th_agenter/services/__pycache__/conversation.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c523063ff37b896ccd06e24906cd0d378e7ce4dd GIT binary patch literal 12864 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 N4Zi}P7d+^a{vXK~G~fUL literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/__pycache__/conversation_context.cpython-313.pyc b/backend/th_agenter/services/__pycache__/conversation_context.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..68f9ce4775bf63c6024512455382789198b78b9f GIT binary patch literal 11162 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 literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/__pycache__/database_config_service.cpython-313.pyc b/backend/th_agenter/services/__pycache__/database_config_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c9a700aab8d41f6caf5610d56bb50dbe831442bf GIT binary patch literal 15423 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 literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/__pycache__/document_processor.cpython-313.pyc b/backend/th_agenter/services/__pycache__/document_processor.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9a927236ecf256876fc802da9a8c883dd89f147c GIT binary patch literal 42476 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 literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/__pycache__/knowledge_base.cpython-313.pyc b/backend/th_agenter/services/__pycache__/knowledge_base.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..56ee1e9861220585985b7c5463f7c45a3900a44c GIT binary patch literal 9712 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 literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/__pycache__/knowledge_chat.cpython-313.pyc b/backend/th_agenter/services/__pycache__/knowledge_chat.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..f712ec7d9881fab675162742a7ebeaa5cef623f5 GIT binary patch literal 15588 zcmbt*3v^r6neIM%OV-PJ`E4D)5-W-2Je-GBwAUh`2i69-D3dNQmjV!tQ$RY7u zN}vpwLQ6uS36w(U+zz3$2C}AIcMN5wA*G8<*W9}#oFes1a?8CWfvk1c8l_1KrCs;l z|KCSPQXB-oEF%by==YKDnux}vb1AI|}CZD)A#1Zlg z5eOv$C#d>3%E43JrwXX4I-sGNfR<_lI;soksh&p~O`joPq(+w3_L%||w1TB|edd6L zS^`#T4cMrS<>~wEfl68#a8L((H}p9JF6v@wW1lNQt0WbBkw7riHG|?uOuIy_Lw9uA7D{W=(j=r|QBD#pBoqdY~ z?X(@zX2I3x3oM~aSlZpUG_Z^=Y(f%rOa3J>GHmK;=WjPWGu?>i-=Kvcs!Ei#V9>6 zF&yQm#fkVB-yeyO^^Qj-;(V}Yr~e8zJK)nwn*IqfDQUJ%3@0UZPyCQ%xN|Bwk%&iP zlC?)X6d&FhjYnxDnV>#S(hE^hghEJ~fLHR)sc5|C4xd^w-#$GViSL5a$XHY|_S~T) zEy5u&8J*0(`c#qw>+PnA$*JU?=;Tx^l8j2ue6hGY78#C?Ct@Q}Y|R;*j>jYWV$t0Z zQB020#PrxWWZQ%!y>mJ_HJ#iYp)f|s()B{=WCT)%-8=7!4nuFR;ABIFef#9T=*S4n zz>Wy!AG(4wj+A0FnS^W+(iQm*1QW3+^wErc3`WJNL|lwYHl;A1flQG=kf(P$rEG3Y zW6@+dmKYn8``(*~ABa*hf^(Utgo+*{8c7UKPe$X(@Dxo9!;~ecMTiAXYBCwALRS6tb!)nn8>LG2HO3>7juWG1H&<^PZ-H>6(2+N6dX#_pX zHwlKJiXromWysQ{!V*@h5{#_WDwu{Upw`V48li$w*cxFk!EUM%EWqD7WMS>thU|hJ z+NwluLM5d1ETtQAbXf%l*8D0*os1sq>7aF;;A9+Kj9={y{L1rkLu+Qa*IlsB@?1U8 zn>yADEcY<(a{rAbBQ)S{W4)*nKn9GNef5&7uxCoX=9Ly00>vdFsYdn<=XWQ(5@Lj( z8h+Ofv-#g^k6|tggP6}kh*NfXh{OuD2X#t$(4eG>&tOF{t)y1x6LHm`Mac`nx5jye zC;N8rROjC{`KMN%cR}B+gZ32(O#T8MhCC0w()yrXNd+BBs`zXsN{^vkrW3Gi(4$ZU zYm`*+DHubf?0nP~X_SxNm)IYZNR9q}|C8#+3a zad)NNUGwfWvnI%MHD(?4$L~%%+On?35NU)JqCVM@D~W~-Y|+?K9hk*#Vvu`FHH znf26Nwdh@@Sn_sR{>3ZSLd8a?c+OA@HS3mUYB#=HyK%mD6Y#QKG+3Xkc&sAj z*?xNDo$yDvXzxBw3IU&wxA~ z#M&$b3o=8=AtYGX$HhE>Tfdi(2#BDfY{8|BVFY&FU_s85lP*H(UQ!pOgR+M~>d34h zF54q(p}3H+5cN842&sYvxlte`uatm{w9Z&qT0ZDJwofZs9mp3U-U@Zqb6bSVu{PbV?5k zqXNlT^0k^2%HmTn<=aC@0|DfpdJu&J3n7s77kjenR5mMnw{GYccvC}ZG>mk8>(zDGZ%mY<_s0hL;3T6ad1`~e2XcoDEylpjq{ zzOY9aRP{@SsWF*mW{U#eP^?bJqI^7&4i=yDi>WE)PzVsZu(kdhJjguf~#;`?Sc%J}i~o6Xaej>%6^ph{|J zcV>x&G=1QjI$`VNmwP&P@2Bu7EChexO5toJi5J75Ap-y{=! zED;&GVZ@Ac-&TG`1vlmZlyYR?9Ns~MTZ8)KSbOm-&8%3~68!_!vrZAku*_G#{ zvN&sDP)48&+2m2&t&%DsN}8!ia$M4UH8ByFtc8ZcELRsj2%`}t{e2M{hpLis`@!KT zvnC`paPt{x7j|VmGP3{*E&^b>1@p{#hHS0S0A^WXTJyQA4K6C8WCQ)h0$qbdm0{GV zrIn>cAPA-|Zb$wXned^$OSM9O> zqy4F-;d8E$Y@PQazh<6acf@kOts~R6Hr=*1)7F=6>r1uvpWT-F(!SL0;Z*a;5!*%Y z;;eUB#=9x)-IVpVy!gPg4`f?cWgDBX8nwLRh&g8^)vYOe%f~uW-T2gjLM!2Ou6v=C z59~GBrk0nhUaESz{-yeJ{%!OA9T|Tp?GL5CJe0a8o*GJ|?w*=&`q~le`&Bi^?mK$l zv9BNf`iXd^YIVA5HF6s`I*@9+GqpRCx^rKuMd`e^ExV{IyLjo#54`liRjr!!#3c9R zlZ&?MYeWT8VRk&(|5$&jZc}RW?$o9)rM@Jb+jDnn@0aKI{8dUAOw|mXGv9OGS@p-9 z7N|cJae+OwY1<0&7T05f#~Cw^aYs)FJl?KzW4yw&-KTzgL+kcd^)FhrkY0e5#$o+{ zi8fq-)4&r|3BF&GAo_~x84}`xc|iyng@CRFLA@ND2O#!9k;FBF1yx#3fr0_93u(Gk zMO7u}1w%I{7(3O9c#CTVQ!R<>_O9uGUJRlEz=EPCL#i&08Ab-d>{o}hNCm>U0Dl#F zLRkF*kXPzsZd|`t>{7K7pJirgFN1n$pQ8Z}mO6u&rD&4R&yZg-i4&6`7wJUuki_l3 zg18?hMk7MGr0cP#uKcBdK{I!d7hcC|k=<>5~eDsZfzVyvk zE=5waMhW;j_2b1@kOdq}WPnS=;e)(IkE+8vq z22Bmk{rKWfe<;_IiJ`y1{G->NyY%V{m!3cM(Q7}seEi$k zql-WL{^chglu0i=`pAbbz52o72QNSW81($&+|wVvdie5B9t1i`Co;c;WQK`OhV!%f z3MW|(z?2r`;VUXhJC2@>8Pi{XZ8|EVHo1SeboC)LL-~4bB}CxlD!(>WwXiOn1rteG zU+~q{1D~o)rW;t|vR#EN*Fn@mcwmc_ZwDNes0>2h!f=2*qvgpikkJ)DJx125N4ZzH z0dO~%rvSAh?oR@ChvlL2FPGFHctvc3=Vj?>+z7}%dT@$vk_O64&AbUhBV*{Dn27r~ z#uCGkn7GLg`g{`R4_Ov73;dpdRz!qxS3tg|8n?XBb*eYx?R?MM`9{|tM4Y;Zs%tqP zNBbcO-PIr;5vMobwTcq{(uV&;=N<>1B3fWzb2w{`Z8*9i-MHoTuKC6-^Uf_P^A_3- zWjA~U>;?O@4Z0_FI*?L(!vYr zDDW2l4k8$J&OqEv8COTz)sd}k&DJ%Z;EuQFw8ZVt5w+ViYs{H-HA`nL*X_j7nyFmz zZsm%s#eL-dl)3G^*?TgcHs6*tx8(GiRi78cMmq?5!|A#``EG`NKy0C}$tsmpER&(>DV*yGXl6GVNu zAPC44*Xe>FX#6FH-mV^~|9Nef(OuZ~2ndW_kz2N}{X*?OC<~|%YCGM`(%m#V6#<)| zoPJ}twx9?716TeZ1UdragT1Gr;HJ&*iFi^nf_E5jLj)w8j(!73Xf;HVCM0YRO1j7t zxHv}W!sa;C>gNYD4J*?PD}PnFQX#82t$wHCt%|g_|2_Mz zt488#f@N*5nzgbeJAgJ&$soWxq0cX3(1*#?>*h97;YQ6;_WH09w zK?jV6Kv@|VZ28*Exs!$m?s235N_sqi|<-~rBs%zltnVf z(l}%jOyJ6L4EmJZ!6iy6h?;mofc_elmGHTg4uukW4DOtTA&3r-1p&TnMK`$B3}oO0 z{0@RsXr)2|PNCair8o!CJZB-eR!J3~f_V^85(^=x(gWrQ16_xKFWrfsOC9CFN^|x& zi!LJsu3!hXpx0QoefBHIV)C)3e3FUhmouK;5>dt+MaN4tRgZpqO`+wBYgz3YZg{fXhe!|nSLFK*nuUD#-ay6 zzZOSCA4Q4@jJ}IxYTzZ+gBymO6-Uu`Bp;;!IL#lU>LYlXJdu!$$wU}VbE6S(j!(o7 zz`+ciMMii+L{aX*^do`=(?W)blZ(U75P=>ALNw z6Z3U<&G!FKr}w;PQEKt#dC!)VeM?S9>iJCFigewI)XLuZx@`)@NpGqKjch z9-H@!r|jc-VwvZbblsLL-+uO6=W-u7j-p*5+Q_hrkb(U{=+5D0@ z+t~DC?AchR(VuSgpPZTNo^RX^cz%iF_e4|ays9SEZNIk@yXSphN5&UO`vO^4!%5pU zwQ7muW1?zxr0x8516i^n<6Hl(Z~a_%WE;qGh`0g~{o#D5q z`R!~X+Sx=bNxPS1n^)zGq-h7tg|^8G-3D{S#h-6jmT6f1Zo}$RvH6DH^UW(V&FkN7 zUZ2{~Ki|9y+<7mqdv;x>&Y!OHXB(Qp1L(3sb&K_?7rJ!SKw9tOKDpisW(NA-`niGw zZta`uxt^cuP903uY&mD{Iqzsrwe`H+ays(!MW?G%ErEGQU&`Fa{BZr;pTq%JDGzbC zcW@7HTfeh~yuFFr>D9cw*$3%gv~xR~G{5lmtcI6&8ja%THNS|Gz2AW|&eT9H&h6V41gzTOeEEJYIECS8OmGi*epozIs?as86hG_%U%*vg4 zp#om@yinnl-N?7Y1rp`P2(uYLR{Q(|Ag%uv`b7ULMn8l|!TW5hGf{y;<3_Ace*jt1 zin6=%vn1O!)D^*{qmS>ou?Yx#%Nby;MiFF~Jf~iIj6X%f= zT*%-UCRQ!L#HHUf&$2A)F5q&&?xZYgE*uTT4M0&*+CMqHcU-|-48cAJROMQ?++33( za;uVG?kG`E10t^onaUyO5=?9sG-Xk<*-7rQh^kKTQ{R-w?h(PoPAHCQ?XSrbNDE zV*na<$p@Y$qy?O(i`xcy)-8z2qhmmGZ3=~85v<_(wv~9k?VV(@DrA5;xf76KWvEiH zGv1ZV69JfiPgQWSQor~tqFt!rU^SfGg%t$&AJA}tm#g4e0Ni;Ceh6+@xCKAJ0r*uy z)!>PD!xbMxs0vaZOvxAQz%OrSet8@7!FMS9l<&ZN@D8CS?hHACol0r(Db$8k%L^j_ zrxRQ^DwS1nSIAZPX0UP54I6TXTph5Qfv0P&2Hdk8j2sT#M3uaLMiGTKnt)EJ83DZg zXYC5-U~yNJLHi-~AfhG~LS|*X3iW=K(9o>`Cvn|a9asPbybl(@81&aYxJIcJEMR>l z2Ub?&;0EO_0un!jH=^C`HS!q+CVAkTL!D5Z&rP z2F@tpfI~i$fwK=didk?h@3=KkUGKfZ(qAm5w2^fy124|8sLdh(g)IrLr}MFgW^ zjJ|=<2u2S>1YUf6(7%D`3p&y%#d<0#V!Z15zX9jp<+J~)^1*FruLVeAHA zrqMobDHT`tY{x=hsVL!z)VWX!tQA( zD~$dlvSJ&g22u;mOyzaU7TMtRe$MWYy8ejjr+q!y+D5#9 z*qyHK$kw!+2&8K|;Synkb#@orAoR3n+^gSpugdAS=>S87Uva**_t*Sdu#77maMI6ZQ+kZf8(q^vO4Q(IuS~{mK@oYt@S?B_lMsOa-?N|ySj%1nv{3f@8Qs)A!~PK z?7pKV9R0zhl$+&XuQboA2y_3n+^`U`f)`A(_`*!bH?&mvBuS?bRpEK{uHm?53@Lbc6M^Eid)o+46JG}E|FRV#R zWyai=Hn(NWE7Il_%y4fj81CS~u&e|xfyMgAoCUc5NsPnFewYiiaF1;5yuFj0S?%80 zZuq%rYdgIBVoi?=9`BemJ3CeHSh<}Yx_4}9OgnU#UtfuF2ZuSGn;?C*Lj|19cA5gb z`fPVkL!eImOOF<^ep#o5tY7kKNK0Bc#ifTRn$0(u)_C-QTJ_pDL9&02;a7m9c*hS# z6b3w|>m;otq$zg{v$sXy;Bt%>jtAhzB0&2ffDouLbugq0qIO&mLi%8-X__yET+}%W z!XQdq7D5IE7?!6DmNE12>W*R}!l7l!{lZ;)@&I4gN&v7HKraAR&;W5`$*pv!(u3kt z(3aejEV$Qi1gvQK!7WBz+}LAm%|91_NBvBPz^>9P)O>iIQPHM6TwY**>k$2bh49qwqutprA}e|9fRw9mGUKwWOM|T6=umk zQLWr!2#7^oxr!oEj!xam^&`cHFS|XL6vo+G;`S^{JBk43O<>J*j@@nafD`Q|UuL$d z=7@;*hQ7JZ6Z z=2P@8%7K%wEoE+1?#OuxvZ?#cmbu7J7tK|tYPOy;_hy~$Z{5Kha-Khln_&PCalLBp z;hL>R@_kIb(6a=RXAGD=V>I;=^%-aL)(z@2t2s=s(PI7vEu?==)R6XRnU`Pwj{%x{ zG6q73;ZyGz0B8q4jt=2_4PL~@vkSP0ZwiOyo3xNNhr?f+j>PgMws3fKf{Mx5L_8W# zK(RiI-u-Zxq6dnug-B9Q#FJEyuP6p&+@`@4$_nOzmmwR1H%YTEk%-Z^u>?U1{Sih# zzzC%U{TGbRLL|Ava5?~giy96`l5lTj-*hr6LbGTKF#O6;3~johZS|>DIRcNlb*Fn@zwJ6Eb9=eP+Sb$C&brPHr-ZxCj{j?8 zdfVRX1Tu3$&ZMn6HIgInm`ehU^X%~1>bC~cn}zF`mun`~4RAFw+uDBB7T~ntX0NHw zX;}&unZupa%V`6t_vVap+C(bdxe7ULhCeVIj~)%@Eb_aRxEqcOM|b69Zm@<-p5Il< zWqVad?N%<&mL(0OwjpO^NfR+yu2--mPuiDZhb*I<*2?;WiF`ellzYRHCZz)v?A5Ha z&ng4lFjXH{AK!D_m^L>5&c;ags)@sS)!3l*ykFOt)4~&2+nqT*rVPYv%V8NH2$OYo zLcqOi>HkZA6H;Dfk^&x_`Q6c^nc*D`&)=49o$32JH*9R!&VYoQ@AW7tN688 zBsM%Aojl~n^B!D$l0620RRk0}5f9`4Qz8@czhkP3*(f#r%KaN!1pBC3B+mjm`2v$D z+X-kQNYx7{X#RhOBD*IgH7qDjus<8Z9cTQvp(tAvd@0T3?etz_6J3!F{#10aEIOT?!j%BeYy`!!kf0kQsuZ2T43c!4xuB8z`R0%;O} z_ySpXfpq6IYR+*%vo>c?bFK@Tb-&XQZuuqBe1Ww8hIC&bofk;k1=4YWtodI?XUer~ z-q>+i_p$C?E$8_y!T6J$j~JZS2*)|Hmdb~AV6%=t!orm^U0Iv+q1!Rj^{Lnj-Fl1Y gwmQgLdfir@oN*YoHmJ|ka~Si6txMHsmT(aNf8s5>umAu6 literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/__pycache__/langchain_chat.cpython-313.pyc b/backend/th_agenter/services/__pycache__/langchain_chat.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a0f6043c0bce08daafbf1ca488e79be13677a2d GIT binary patch literal 16015 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 MckKU+^mN~U0@e#ZCIA2c literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/__pycache__/llm_service.cpython-313.pyc b/backend/th_agenter/services/__pycache__/llm_service.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..badb7fc18161de3786a667c6333e4aaae30713f1 GIT binary patch literal 5180 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 VT;ex9Zu#62W*FfUf>ig#KL9y0girtg literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/__pycache__/mysql_tool_manager.cpython-313.pyc b/backend/th_agenter/services/__pycache__/mysql_tool_manager.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e67c5385c67c72692a519e93f6de8caa7e769985 GIT binary patch literal 2466 zcmZ`(-ES0C6u+}GyF2^Q-O>+P+tQuVLc0Yv$VWt^#9Be2?N&Rj31MWMZl`s1I=kGN z1xtb~4@euu_Q8@85`zy$hzXJKfGE-UAM9F`WdaFC7UYSQ4T0#3=g!QwrNB*l=j=J> z-gD3So!=={RCp1rJrC9=zI7t>7x__MZd)k*0m2O85k@@4JG!ae4(#9%PxDMSy_>-d zhXiIN;+>l*>jZW-5>HpRYquM_!PAY|5CRW>9pWoCJ8Z8Cp<*T4YzM&sJ|MU^Irv~5 z`jElFQEw9xJZ>aK5W-%;*}vR&T=d~t!M_p--g4Xk;yoMb@-K+%#ctk**)Wm3UEWxi z^&O!`PzN1GBUGd^-3S-}=se=I;5g?@oVOICb_O2^pab zwlAe8lB%TKBN>3gJ1Xmr_L4Gf|V3I|(I#Aj5R$&l20t%}^Mf)HsAHq)}W+JzG{w}1X@ zzUDE*BPLW$m(;jy_{4?c#_b-1I}oDn|I$7Xwo^0k{C=Wevf?!e?X|lOp#f?Tz2_mR zmOEt-Df1_ueFLOJ#UeEJfuG^j(kVdPEsCmqP!uB$gN`d2JXnk?lBS8GMq+UpBer9! zkW9&H=cuge>&B#nD(D}qgVs=W84E*F{3BT%29;P=X)->Lh|8L=kZ-w}qN#BlB+)}l ziyF1x5dy15FnpXrd0$|vW3nUX<8CxGoqF?B-`TFYhV>I&pZDg2Ra1v24`-m?Sy;Up%5&v4nqW)kX1@*HI_nyidyBnb|aHa5HqAQ|;ffwEvE zkI}ZlmO+nPsIuj^U~|5D`7K}Yny)@b{+e%u*PQ-j;*G2Rh8zB>sqV?{Z0O~y{w?`n z^|fF)7Yt|FFoqUJPk=;TLi)c{2ix;HpY&}SCg}4@b^pplVdA^O^octsFWFp@WWOlxPfLnz35w!K0&BXGP-Qg%dwd*SXWDN?H)M8Ae4Gun=YnhIm<{>%Xr_BQG`lvptaXlwK4fU;ilP^}0!0Vw zXYZg@% zT@ETT>wk#g^SIbeA-1}NXsRjCc#p@9#B-k~ izcZo#if-`zS0hiHWnV#;U7c<8FX7H+`qySBi2nm?<#%oX literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/__pycache__/postgresql_tool_manager.cpython-313.pyc b/backend/th_agenter/services/__pycache__/postgresql_tool_manager.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..cf499695babe625515983b38e52d5cbbcbd2682a GIT binary patch literal 2580 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 appW#jppT4l_VF+Jm9eV0FFA7Au>OB}`Tqt0 literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/__pycache__/smart_excel_workflow.cpython-313.pyc b/backend/th_agenter/services/__pycache__/smart_excel_workflow.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..e91527ec7f6873c01b488c1e0f0d9017d12b2b14 GIT binary patch literal 54391 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 SnL3`rmD(nSoX)CI$o~&of7uBD literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/__pycache__/smart_workflow.cpython-313.pyc b/backend/th_agenter/services/__pycache__/smart_workflow.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..690590927949c4844518c5303dcb350b9cb5ea6c GIT binary patch literal 3550 zcmbtW>u(!X7Qc619#8B%+&Cqv6P&by4uO_7i3;1Yt5z*q+>)(2Yc^UejT~p<8k}+V z&bX!D%2TNXq7oaRD|QRCg+*Ew_QR?XZU2YO7L;g~)go=3G@t6MK^5QjoSCs>J81;k zEBnm3_ndpqo%=hFsf5D{fzfifbn+T-|G`Oh2tHGJ37A=;5se-t3Cd`KV_bq~d_rJC zLS$kbW!zDTNgA&SM*|6&$*{(2;?ZD2VM?5&q!V3Lvun=*k_wGbZ_dIGkO87eJwywP zaQ<3rmQ&>woT#EB9L{BIhd*eZafD+z%XZ{nP20JGna(?6(gbF71#PL+>DH#w%s%o` z#>oGsz+Rlp7fwH$Hq%*yfiUI@52x+)c-k_W zYuAJx@&aXIAL%1nhbE>ZO-co{KuS&pMA)Orso=1z1wl&yJ*7!cS*NRLExv9`lWv1e zEtCqSq*QpALodUM7WQ|wHtkyHp=oDGYxQ@wX>F`M%c~Km)4iXjds)FKDe6F!r3+u) zdG%)L+LhZkKUn(P%ga|kT0Z|q>B66tyGrMoxYEPeX>rE~8uU%ya# z{o3uD?=1h}H5ZVjnRl0FzgT+fYU#^&HZU@p@v#S2gz)~qavYdhGS;5QRd+m(GIu0t zHHMNTU)%kBU_*H|rb(83Mn@s4xQTHV1XZWizJ!j@@U3YpnLSt7|?4F??&Ty72MmpaNeABmYj=R;je@6I9-u6xZQ*-0D`VY6?Jx|m?T3Z~8j17pb&AgNqhg4dVp7~EoFcPORGx*` zlgVg{Lb7xpKS@=t_)D;(K-86Pyj{Bd8U&sfQ*KDEymu8N>*AF=pMP8*f3C_#p{8=I z1ME3H)2skESh^Ph?)B)F&5ZPvDzPn~!X7}dbCxEN~mgP>;+rJRoJs;b>5KGL*5{sd(4Bs>@K8vJwyZD*P+Y0r{y}nWRZB$tMMkJ}Eo{&AdJ*cVHF@ZU*wjaWJlUxwjr1DDt=G zuHX6O9F1i?PAQ_)s}W zx}GW%u?x~vY*&B_Dyzu-V1;1+D{+;6Odl35i7IW#JQ;*8a=!)Ury@5NN%9&UJ2K+u zGN9K`QR~p-iV1K~MX%b0*TYy}J%3b=bye|b4EpuevoX2WmCi@tc+TLaz%+Vz8a7x6 zP#rdd^U=YD=%M-OAupf4`1RZ`Dm;gva0m*ApaAECGB8LlQ&qSk4pNn8STfw!?zRSo zpMWYi`n1}CJ=si88PJmzU7spU6!XZ3bZDpPyf10f^~oHw?0n8N%mRo5x;|0J=sLst z?(oo596o2-46$;A@j@Z*whM;!gz-qQGcpWN=3YirhfV~YIisg-o8`ufwqfb|XM}A< z6}R7DXfY%T{H=#5A#adXju++c6ka?;%MtR(uEibdYD-w`{#S%fF_k4Ur}Ku#t- z10Qa`&|41TOd)*(-=k)Er@t-S>TiRmzYRH=^v26U-#>8E4!*E3HyfNS$hFAgeSqZwUgisPf`ve*IH-t zUK_3uAl?UQ2SbRKOs9j(ZqP| zW6t}E3}y}c7;HBfdlYR+XbUDTawF)chNdGQ@M7!d>;MP=OkzC_q|8%F|4oLzCPV)u lt^XqX;rDH@eIeNMk6_RHPuvRbI4j+gpP^LwFG1>B{2v?N<@Epn literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/__pycache__/storage.cpython-313.pyc b/backend/th_agenter/services/__pycache__/storage.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..497e474ad097759668e49af11bd3a8771ed5aaf1 GIT binary patch literal 13479 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 D!RN4Y literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/__pycache__/zhipu_embeddings.cpython-313.pyc b/backend/th_agenter/services/__pycache__/zhipu_embeddings.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..30ef39ab65d6a8106a08de51334aef5c498f5d30 GIT binary patch literal 4239 zcmd57(25q`_vrT8aNf0Rf`mRFSgL#O(2EdR+?Y^oMyONtV3L%^*WELP--T7_ED zZ7g;Q$5_9Sp@MJ3xWTHyvfP2dkG}#X2pSZHzWZdhm^g9JFaKomu`_wCc2n z0(}JEn>TOX-^@4jW_KDJT?C5z!e1A^b`tUpb}EH06Fb*{xJyJriO7hy8HO^@vNN`6 zma@|vvkHRZHqZR)iUJjsHhREKQem^pbuW;RGwg|D_e?%mz85N*^dUOvk*eD^ixGOny0? zP*SEVp)M<$4p!0Qc)dzN8X>S_19;peQD&SuTpmkw5h9Q)6-g37qt%rNSI2;El%&d_ zBIL4=s!CiQNmX~gd_HBb%)H{QP*D=8aw|MsFXE{6DwN2aw!u!-kt?m0k%+HC)mjl` zBbM{kct|yoW44N82qZ~DETEV+!3f1ul}%6vea$|Cs;P!5r_>cCA^c1=76nVoV?xMH z@kFMx3SG0YgyphjYHQPG+@%F8REuekr&L8VOncb}I8|Fnhj^3ImDIvs({!9BU@_mF z9E+vVGn25w;Igc0_1$?n{(+(;EVd-0SWH*wH8rm2v3h44$^d{Q3GXOLX78Q|9jc7u zEp!R0k4UkJ`1j{KCw_T29~{jENAtmnTyWwxcgIuk3wIg|zOFm2Lf60@@3$R2g?9hi z?CNa67g&3L_5J5O+vx&9_X`(sdOvZ0?B4E}e4O~*($>=B*mis54-Rn$wjd^NDAx_$ zv{S{%QZ4>6B2n)al22Jk?j^%SvqhOUa#0~fW5NEoLS5Mt(^Zt#g#QgPan&jUYtk6F zPjea+rH0S9lkj+ePeyG6q>o%d)mx>3$h~k!{s(-3 zi}Jhiu0UMW_%Oe~ERb<_KUrWx_LWu(=7O%sG`=V#(s6yrM$5IP8HcuYCd7w zRM9gK?i7tM~qqdqsOxEPi8-!-R?fIadjiU z9T@wF`Dc-b!GGweiCf{J@KLgL)HM-cx6TA6+S%W>^FY%hFu2QQ8h~NB3@N+J z|HMg{*E)#}oMex{Ty}>&9L0iu)lJIKXKT(7L9Emjk&Ci>K%Wh>yP)4Pi3x{rHLK7Y z0)l9W3a}FG!^#47XFaejSp>Dq0<{G|dI05X;r$SH@l^CMR)}31f-1z7kc)PqZ(#K! zsC0ZVYe;pMAZYDl_Pq|JmdGfsa2+Z{s0$F<*}Zmi^=7_vFxNSl?;Oo_jutw))|OV6 zph3`{yLYZ290FYWj{`3KO}8%sF57$7##hH5wB*|Q^KFBl5$;%XN@$agK)wSTZ>92hjKc4S~UPQ%XOi}Jh-=Q z>*Xe36&CNs`G;?FVv=LYF~*&r{uCD?-=gR4+f1VvQP5vVFkw;bvOFm~(Q zQ`Tk4mx8a=w(NtXkJZPY2;8`8-{doI3vsY@U_vEQV76DcJP==AUuScF+bDfWd-sx zmn6ZgYzk?QBwfwQsq%vbsS{R@Dp&i9VqS&r-NU z{95{?Cy_b>mFbft-GDE?xFj0}Rp+yYqD#_4LVHjI-&Bg&vMT&|(SHrqFUXhN>7s)K z4;K7y6?{Q>3ZJ`M`1hEflXGs?idimA| z@2&TL{*xjBa$|Ip+l)SP=Z;-?f&F681LJIwK(o%RM<2RhAX)5T`L~(%3ma_@FBJ*W zn**E1I9oFGTXY5n;K?RB9;(PP4D%&9`weOOGwFLu0#C`vQxbei4t(uw b+ipMj#Chl=`wROxbCLOM`a6QHwdlV902ih7 literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/agent/__init__.py b/backend/th_agenter/services/agent/__init__.py new file mode 100644 index 0000000..62f5fd3 --- /dev/null +++ b/backend/th_agenter/services/agent/__init__.py @@ -0,0 +1,10 @@ +"""Agent services package.""" + +from .base import BaseTool, ToolRegistry +from .agent_service import AgentService + +__all__ = [ + "BaseTool", + "ToolRegistry", + "AgentService" +] \ No newline at end of file diff --git a/backend/th_agenter/services/agent/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/services/agent/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6747786020a4b4f9bf2f0c2c29c873212c22f477 GIT binary patch literal 371 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 Nnm&vCvY7$+e*nrJl1Kmm literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/agent/__pycache__/base.cpython-313.pyc b/backend/th_agenter/services/agent/__pycache__/base.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4d7843913f1645f44b0ed4c8411b964e97ab7d83 GIT binary patch literal 11946 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 str: + """Synchronous run method.""" + # Handle both positional and keyword arguments + if args: + # If positional arguments are provided, convert them to kwargs + # based on the tool's parameter names + params = self.base_tool.get_parameters() + for i, arg in enumerate(args): + if i < len(params): + kwargs[params[i].name] = arg + + # Run async method in sync context + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + result = loop.run_until_complete(self.base_tool.execute(**kwargs)) + return self._format_result(result) + finally: + loop.close() + + async def _arun(self, *args, **kwargs) -> str: + """Asynchronous run method.""" + # Handle both positional and keyword arguments + if args: + # If positional arguments are provided, convert them to kwargs + # based on the tool's parameter names + params = self.base_tool.get_parameters() + for i, arg in enumerate(args): + if i < len(params): + kwargs[params[i].name] = arg + + result = await self.base_tool.execute(**kwargs) + return self._format_result(result) + + def _format_result(self, result: ToolResult) -> str: + """Format tool result for LangChain.""" + if result.success: + if isinstance(result.result, dict) and "summary" in result.result: + return result.result["summary"] + return str(result.result) + else: + return f"Error: {result.error}" + + +class AgentConfig(BaseModel): + """Agent configuration.""" + enabled_tools: List[str] = Field(default_factory=lambda: [ + "calculator", "weather", "search", "datetime", "file", "generate_image", "postgresql_mcp", "mysql_mcp" + ]) + max_iterations: int = Field(default=10) + temperature: float = Field(default=0.1) + system_message: str = Field( + default="You are a helpful AI assistant with access to various tools. " + "Use the available tools to help answer user questions accurately. " + "Always explain your reasoning and the tools you're using." + ) + verbose: bool = Field(default=True) + + +class AgentService: + """LangChain Agent service with tool calling capabilities.""" + + def __init__(self, db_session=None): + self.settings = get_settings() + self.tool_registry = ToolRegistry() + self.config = AgentConfig() + self.agent_executor: Optional[AgentExecutor] = None + self.db_session = db_session + self.config_service = AgentConfigService(db_session) if db_session else None + self._initialize_tools() + self._load_config() + + def _initialize_tools(self): + """Initialize and register all available tools.""" + tools = [ + WeatherQueryTool(), + TavilySearchTool(), + DateTimeTool(), + get_postgresql_tool(), # 使用单例PostgreSQL MCP工具 + get_mysql_tool() # 使用单例MySQL MCP工具 + ] + + for tool in tools: + self.tool_registry.register(tool) + logger.info(f"Registered tool: {tool.get_name()}") + + def _load_config(self): + """Load configuration from database if available.""" + if self.config_service: + try: + config_dict = self.config_service.get_config_dict() + # Update config with database values + for key, value in config_dict.items(): + if hasattr(self.config, key): + setattr(self.config, key, value) + logger.info("Loaded agent configuration from database") + except Exception as e: + logger.warning(f"Failed to load config from database, using defaults: {str(e)}") + + def _get_enabled_tools(self) -> List[LangChainToolWrapper]: + """Get list of enabled LangChain tools.""" + enabled_tools = [] + + for tool_name in self.config.enabled_tools: + tool = self.tool_registry.get_tool(tool_name) + if tool: + langchain_tool = LangChainToolWrapper(base_tool=tool) + enabled_tools.append(langchain_tool) + logger.debug(f"Enabled tool: {tool_name}") + else: + logger.warning(f"Tool not found: {tool_name}") + + return enabled_tools + + def _create_agent_executor(self) -> AgentExecutor: + """Create LangChain agent executor.""" + if not LANGCHAIN_CLASSIC_AVAILABLE: + raise ValueError("Agent functionality is disabled because langchain_classic is not available.") + + # Get LLM configuration + from ...core.llm import create_llm + llm = create_llm() + + # Get enabled tools + tools = self._get_enabled_tools() + + # Create prompt template + prompt = ChatPromptTemplate.from_messages([ + ("system", self.config.system_message), + MessagesPlaceholder(variable_name="chat_history"), + ("human", "{input}"), + MessagesPlaceholder(variable_name="agent_scratchpad") + ]) + + # Create agent + agent = create_tool_calling_agent(llm, tools, prompt) + + # Create agent executor + agent_executor = AgentExecutor( + agent=agent, + tools=tools, + max_iterations=self.config.max_iterations, + verbose=self.config.verbose, + return_intermediate_steps=True + ) + + return agent_executor + + async def chat(self, message: str, chat_history: Optional[List[Dict[str, str]]] = None) -> Dict[str, Any]: + """Process chat message with agent.""" + try: + logger.info(f"Processing agent chat message: {message[:100]}...") + + # Create agent executor if not exists + if not self.agent_executor: + self.agent_executor = self._create_agent_executor() + + # Convert chat history to LangChain format + langchain_history = [] + if chat_history: + for msg in chat_history: + if msg["role"] == "user": + langchain_history.append(HumanMessage(content=msg["content"])) + elif msg["role"] == "assistant": + langchain_history.append(AIMessage(content=msg["content"])) + + # Execute agent + result = await self.agent_executor.ainvoke({ + "input": message, + "chat_history": langchain_history + }) + + # Extract response and intermediate steps + response = result["output"] + intermediate_steps = result.get("intermediate_steps", []) + + # Format tool calls for response + tool_calls = [] + for step in intermediate_steps: + if len(step) >= 2: + action, observation = step[0], step[1] + tool_calls.append({ + "tool": action.tool, + "input": action.tool_input, + "output": observation + }) + + logger.info(f"Agent response generated successfully with {len(tool_calls)} tool calls") + + return { + "response": response, + "tool_calls": tool_calls, + "success": True + } + + except Exception as e: + logger.error(f"Agent chat error: {str(e)}", exc_info=True) + return { + "response": f"Sorry, I encountered an error: {str(e)}", + "tool_calls": [], + "success": False, + "error": str(e) + } + + async def chat_stream(self, message: str, chat_history: Optional[List[Dict[str, str]]] = None) -> AsyncGenerator[Dict[str, Any], None]: + """Process chat message with agent (streaming).""" + tool_calls = [] # Initialize tool_calls at the beginning + try: + logger.info(f"Processing agent chat stream: {message[:100]}...") + + # Create agent executor if not exists + if not self.agent_executor: + self.agent_executor = self._create_agent_executor() + + # Convert chat history to LangChain format + langchain_history = [] + if chat_history: + for msg in chat_history: + if msg["role"] == "user": + langchain_history.append(HumanMessage(content=msg["content"])) + elif msg["role"] == "assistant": + langchain_history.append(AIMessage(content=msg["content"])) + + # Yield initial status + yield { + "type": "status", + "content": "🤖 开始分析您的请求...", + "done": False + } + await asyncio.sleep(0.2) + + # Use astream_events for real streaming (if available) or fallback to simulation + try: + # Try to use streaming events if available + async for event in self.agent_executor.astream_events( + {"input": message, "chat_history": langchain_history}, + version="v1" + ): + if event["event"] == "on_tool_start": + tool_name = event["name"] + yield { + "type": "tool_start", + "content": f"🔧 正在使用工具: {tool_name}", + "tool_name": tool_name, + "done": False + } + await asyncio.sleep(0.1) + + elif event["event"] == "on_tool_end": + tool_name = event["name"] + yield { + "type": "tool_end", + "content": f"✅ 工具 {tool_name} 执行完成", + "tool_name": tool_name, + "done": False + } + await asyncio.sleep(0.1) + + elif event["event"] == "on_chat_model_stream": + chunk = event["data"]["chunk"] + if hasattr(chunk, 'content') and chunk.content: + yield { + "type": "content", + "content": chunk.content, + "done": False + } + await asyncio.sleep(0.05) + + except Exception as stream_error: + logger.warning(f"Streaming events not available, falling back to simulation: {stream_error}") + + # Fallback: Execute agent and simulate streaming + result = await self.agent_executor.ainvoke({ + "input": message, + "chat_history": langchain_history + }) + + # Extract response and intermediate steps + response = result["output"] + intermediate_steps = result.get("intermediate_steps", []) + + # Yield tool execution steps + tool_calls = [] + for i, step in enumerate(intermediate_steps): + if len(step) >= 2: + action, observation = step[0], step[1] + tool_calls.append({ + "tool": action.tool, + "input": action.tool_input, + "output": observation + }) + + # Yield tool execution status + yield { + "type": "tool", + "content": f"🔧 使用工具 {action.tool}: {str(action.tool_input)[:100]}...", + "tool_name": action.tool, + "tool_input": action.tool_input, + "done": False + } + await asyncio.sleep(0.3) + + yield { + "type": "tool_result", + "content": f"✅ 工具结果: {str(observation)[:200]}...", + "tool_name": action.tool, + "done": False + } + await asyncio.sleep(0.2) + + # Yield thinking status + yield { + "type": "thinking", + "content": "🤔 正在整理回答...", + "done": False + } + await asyncio.sleep(0.3) + + # Yield the final response in chunks to simulate streaming + words = response.split() + current_content = "" + + for i, word in enumerate(words): + current_content += word + " " + + # Yield every 2-3 words or at the end + if (i + 1) % 2 == 0 or i == len(words) - 1: + yield { + "type": "response", + "content": current_content.strip(), + "tool_calls": tool_calls if i == len(words) - 1 else [], + "done": i == len(words) - 1 + } + + # Small delay to simulate typing + if i < len(words) - 1: + await asyncio.sleep(0.05) + + logger.info(f"Agent stream response completed with {len(tool_calls)} tool calls") + + except Exception as e: + logger.error(f"Agent chat stream error: {str(e)}", exc_info=True) + yield { + "type": "error", + "content": f"Sorry, I encountered an error: {str(e)}", + "done": True + } + + def update_config(self, config: Dict[str, Any]): + """Update agent configuration.""" + try: + # Update configuration + for key, value in config.items(): + if hasattr(self.config, key): + setattr(self.config, key, value) + logger.info(f"Updated agent config: {key} = {value}") + + # Reset agent executor to apply new config + self.agent_executor = None + + except Exception as e: + logger.error(f"Error updating agent config: {str(e)}", exc_info=True) + raise + + def load_config_from_db(self, config_id: Optional[int] = None): + """Load configuration from database.""" + if not self.config_service: + logger.warning("No database session available for loading config") + return + + try: + config_dict = self.config_service.get_config_dict(config_id) + self.update_config(config_dict) + logger.info(f"Loaded configuration from database (ID: {config_id})") + except Exception as e: + logger.error(f"Error loading config from database: {str(e)}") + raise + + def get_available_tools(self) -> List[Dict[str, Any]]: + """Get list of available tools.""" + tools = [] + for tool_name, tool in self.tool_registry._tools.items(): + tools.append({ + "name": tool.get_name(), + "description": tool.get_description(), + "parameters": [{ + "name": param.name, + "type": param.type.value, + "description": param.description, + "required": param.required, + "default": param.default, + "enum": param.enum + } for param in tool.get_parameters()], + "enabled": tool_name in self.config.enabled_tools + }) + return tools + + def get_config(self) -> Dict[str, Any]: + """Get current agent configuration.""" + return self.config.dict() + + +# Global agent service instance +_agent_service: Optional[AgentService] = None + + +def get_agent_service(db_session=None) -> Optional[AgentService]: + """Get global agent service instance.""" + global _agent_service + if _agent_service is None: + try: + _agent_service = AgentService(db_session) + except Exception as e: + logger.warning(f"Failed to initialize AgentService: {str(e)}. Agent functionality will be disabled.") + _agent_service = None + elif db_session and _agent_service and not _agent_service.db_session: + # Update with database session if not already set + _agent_service.db_session = db_session + _agent_service.config_service = AgentConfigService(db_session) + _agent_service._load_config() + return _agent_service \ No newline at end of file diff --git a/backend/th_agenter/services/agent/base.py b/backend/th_agenter/services/agent/base.py new file mode 100644 index 0000000..6c5e163 --- /dev/null +++ b/backend/th_agenter/services/agent/base.py @@ -0,0 +1,248 @@ +"""Base classes for Agent tools.""" + +import json +from abc import ABC, abstractmethod +from typing import Any, Dict, List, Optional, Type, Callable +from pydantic import BaseModel, Field +from dataclasses import dataclass +from enum import Enum + +from ...utils.logger import get_logger + +logger = get_logger("agent_tools") + + +class ToolParameterType(str, Enum): + """Tool parameter types.""" + STRING = "string" + INTEGER = "integer" + FLOAT = "float" + BOOLEAN = "boolean" + ARRAY = "array" + OBJECT = "object" + + +@dataclass +class ToolParameter: + """Tool parameter definition.""" + name: str + type: ToolParameterType + description: str + required: bool = True + default: Any = None + enum: Optional[List[Any]] = None + + def to_dict(self) -> Dict[str, Any]: + """Convert to dictionary for JSON schema.""" + param_dict = { + "type": self.type.value, + "description": self.description + } + + if self.enum: + param_dict["enum"] = self.enum + + if self.default is not None: + param_dict["default"] = self.default + + return param_dict + + +class ToolResult(BaseModel): + """Tool execution result.""" + success: bool = Field(description="Whether the tool execution was successful") + result: Any = Field(description="The result data") + error: Optional[str] = Field(default=None, description="Error message if failed") + metadata: Optional[Dict[str, Any]] = Field(default=None, description="Additional metadata") + + +class BaseTool(ABC): + """Base class for all Agent tools.""" + + def __init__(self): + self.name = self.get_name() + self.description = self.get_description() + self.parameters = self.get_parameters() + + @abstractmethod + def get_name(self) -> str: + """Get tool name.""" + pass + + @abstractmethod + def get_description(self) -> str: + """Get tool description.""" + pass + + @abstractmethod + def get_parameters(self) -> List[ToolParameter]: + """Get tool parameters.""" + pass + + @abstractmethod + async def execute(self, **kwargs) -> ToolResult: + """Execute the tool with given parameters.""" + pass + + def get_schema(self) -> Dict[str, Any]: + """Get tool schema for LangChain.""" + properties = {} + required = [] + + for param in self.parameters: + properties[param.name] = param.to_dict() + if param.required: + required.append(param.name) + + return { + "type": "function", + "function": { + "name": self.name, + "description": self.description, + "parameters": { + "type": "object", + "properties": properties, + "required": required + } + } + } + + def validate_parameters(self, **kwargs) -> Dict[str, Any]: + """Validate and process input parameters.""" + validated = {} + + for param in self.parameters: + value = kwargs.get(param.name) + + # Check required parameters + if param.required and value is None: + raise ValueError(f"Required parameter '{param.name}' is missing") + + # Use default if not provided + if value is None and param.default is not None: + value = param.default + + # Type validation (basic) + if value is not None: + if param.type == ToolParameterType.INTEGER and not isinstance(value, int): + try: + value = int(value) + except (ValueError, TypeError): + raise ValueError(f"Parameter '{param.name}' must be an integer") + + elif param.type == ToolParameterType.FLOAT and not isinstance(value, (int, float)): + try: + value = float(value) + except (ValueError, TypeError): + raise ValueError(f"Parameter '{param.name}' must be a number") + + elif param.type == ToolParameterType.BOOLEAN and not isinstance(value, bool): + if isinstance(value, str): + value = value.lower() in ('true', '1', 'yes', 'on') + else: + value = bool(value) + + # Enum validation + if param.enum and value not in param.enum: + raise ValueError(f"Parameter '{param.name}' must be one of {param.enum}") + + validated[param.name] = value + + return validated + + +class ToolRegistry: + """Registry for managing Agent tools.""" + + def __init__(self): + self._tools: Dict[str, BaseTool] = {} + self._enabled_tools: Dict[str, bool] = {} + + def register(self, tool: BaseTool, enabled: bool = True) -> None: + """Register a tool.""" + tool_name = tool.get_name() + self._tools[tool_name] = tool + self._enabled_tools[tool_name] = enabled + logger.info(f"Registered tool: {tool_name} (enabled: {enabled})") + + def unregister(self, tool_name: str) -> None: + """Unregister a tool.""" + if tool_name in self._tools: + del self._tools[tool_name] + del self._enabled_tools[tool_name] + logger.info(f"Unregistered tool: {tool_name}") + + def get_tool(self, tool_name: str) -> Optional[BaseTool]: + """Get a tool by name.""" + return self._tools.get(tool_name) + + def get_enabled_tools(self) -> Dict[str, BaseTool]: + """Get all enabled tools.""" + return { + name: tool for name, tool in self._tools.items() + if self._enabled_tools.get(name, False) + } + + def get_all_tools(self) -> Dict[str, BaseTool]: + """Get all registered tools.""" + return self._tools.copy() + + def enable_tool(self, tool_name: str) -> None: + """Enable a tool.""" + if tool_name in self._tools: + self._enabled_tools[tool_name] = True + logger.info(f"Enabled tool: {tool_name}") + + def disable_tool(self, tool_name: str) -> None: + """Disable a tool.""" + if tool_name in self._tools: + self._enabled_tools[tool_name] = False + logger.info(f"Disabled tool: {tool_name}") + + def is_enabled(self, tool_name: str) -> bool: + """Check if a tool is enabled.""" + return self._enabled_tools.get(tool_name, False) + + def get_tools_schema(self) -> List[Dict[str, Any]]: + """Get schema for all enabled tools.""" + enabled_tools = self.get_enabled_tools() + return [tool.get_schema() for tool in enabled_tools.values()] + + async def execute_tool(self, tool_name: str, **kwargs) -> ToolResult: + """Execute a tool with given parameters.""" + tool = self.get_tool(tool_name) + + if not tool: + return ToolResult( + success=False, + result=None, + error=f"Tool '{tool_name}' not found" + ) + + if not self.is_enabled(tool_name): + return ToolResult( + success=False, + result=None, + error=f"Tool '{tool_name}' is disabled" + ) + + try: + # Validate parameters + validated_params = tool.validate_parameters(**kwargs) + + # Execute tool + result = await tool.execute(**validated_params) + logger.info(f"Tool '{tool_name}' executed successfully") + return result + + except Exception as e: + logger.error(f"Tool '{tool_name}' execution failed: {str(e)}", exc_info=True) + return ToolResult( + success=False, + result=None, + error=f"Tool execution failed: {str(e)}" + ) + + +# Global tool registry instance +tool_registry = ToolRegistry() \ No newline at end of file diff --git a/backend/th_agenter/services/agent/langgraph_agent_service.py b/backend/th_agenter/services/agent/langgraph_agent_service.py new file mode 100644 index 0000000..d956586 --- /dev/null +++ b/backend/th_agenter/services/agent/langgraph_agent_service.py @@ -0,0 +1,441 @@ +"""LangGraph Agent service with tool calling capabilities.""" + +import asyncio +from typing import List, Dict, Any, Optional, AsyncGenerator +from langchain_core.messages import HumanMessage, AIMessage +from langchain_core.tools import tool + +# Import logger first +from ...utils.logger import get_logger +logger = get_logger("langgraph_agent_service") + +# Try to import langgraph related modules +try: + from langgraph.prebuilt import create_react_agent + LANGGRAPH_AVAILABLE = True +except ImportError: + logger.warning("langgraph not available. LangGraph agent functionality will be disabled.") + create_react_agent = None + LANGGRAPH_AVAILABLE = False + +# Try to import init_chat_model from langchain_openai +try: + from langchain_openai import ChatOpenAI + # Create a simple init_chat_model function as a replacement + def init_chat_model(model_name: str, **kwargs): + return ChatOpenAI(model=model_name, **kwargs) +except ImportError: + logger.warning("langchain_openai not available. Chat model functionality may be limited.") + init_chat_model = None +from pydantic import BaseModel, Field + +from .base import ToolRegistry +from th_agenter.services.tools import WeatherQueryTool, TavilySearchTool, DateTimeTool +from ..postgresql_tool_manager import get_postgresql_tool +from ...core.config import get_settings +from ...utils.logger import get_logger +from ..agent_config import AgentConfigService + +logger = get_logger("langgraph_agent_service") + + + +class LangGraphAgentConfig(BaseModel): + """LangGraph Agent configuration.""" + model_name: str = Field(default="gpt-3.5-turbo") + model_provider: str = Field(default="openai") + base_url: Optional[str] = Field(default=None) + api_key: Optional[str] = Field(default=None) + enabled_tools: List[str] = Field(default_factory=lambda: [ + "calculator", "weather", "search", "file", "image" + ]) + max_iterations: int = Field(default=10) + temperature: float = Field(default=0.7) + max_tokens: int = Field(default=1000) + system_message: str = Field( + default="""你是一个有用的AI助手,可以使用各种工具来帮助用户解决问题。 + 重要规则: + 1. 工具调用失败时,必须仔细分析失败原因,特别是参数格式问题 + 3. 在重新调用工具前,先解释上次失败的原因和改进方案 + 4. 确保每个工具调用的参数格式严格符合工具的要求 """ + ) + verbose: bool = Field(default=True) + + +class LangGraphAgentService: + """LangGraph Agent service using create_react_agent.""" + + def __init__(self, db_session=None): + self.settings = get_settings() + self.tool_registry = ToolRegistry() + self.config = LangGraphAgentConfig() + + # Check if langgraph is available + if not LANGGRAPH_AVAILABLE: + logger.warning("LangGraph is not available. Some features may be disabled.") + self.tools = [] + self.db_session = db_session + self.config_service = AgentConfigService(db_session) if db_session else None + self._initialize_tools() + self._load_config() + self._create_agent() + + def _initialize_tools(self): + """Initialize available tools.""" + # Use the @tool decorated functions + self.tools = [ + + WeatherQueryTool(), + TavilySearchTool(), + DateTimeTool() + ] + + + + def _load_config(self): + """Load configuration from database if available.""" + if self.config_service: + try: + db_config = self.config_service.get_active_config() + if db_config: + # Update config with database values + config_dict = db_config.config_data + for key, value in config_dict.items(): + if hasattr(self.config, key): + setattr(self.config, key, value) + logger.info("Loaded configuration from database") + except Exception as e: + logger.warning(f"Failed to load config from database: {e}") + + + + def _create_agent(self): + """Create LangGraph agent using create_react_agent.""" + try: + # Initialize the model + llm_config = get_settings().llm.get_current_config() + self.model = init_chat_model( + model=llm_config['model'], + model_provider='openai', + temperature=llm_config['temperature'], + max_tokens=llm_config['max_tokens'], + base_url= llm_config['base_url'], + api_key=llm_config['api_key'] + ) + + + + # Create the react agent + self.agent = create_react_agent( + model=self.model, + tools=self.tools,) + + logger.info("LangGraph React agent created successfully") + + except Exception as e: + logger.error(f"Failed to create agent: {str(e)}") + raise + + + + + + + + def _format_tools_info(self) -> str: + """Format tools information for the prompt.""" + tools_info = [] + for tool_name in self.config.enabled_tools: + tool = self.tool_registry.get_tool(tool_name) + if tool: + params_info = [] + for param in tool.get_parameters(): + params_info.append(f" - {param.name} ({param.type.value}): {param.description}") + + tool_info = f"**{tool.get_name()}**: {tool.get_description()}" + if params_info: + tool_info += "\n" + "\n".join(params_info) + tools_info.append(tool_info) + + return "\n\n".join(tools_info) + + + + async def chat(self, message: str, chat_history: Optional[List[Dict[str, str]]] = None) -> Dict[str, Any]: + """Process a chat message using LangGraph.""" + try: + logger.info(f"Starting chat with message: {message[:100]}...") + + # Convert chat history to messages + messages = [] + if chat_history: + for msg in chat_history: + if msg["role"] == "user": + messages.append(HumanMessage(content=msg["content"])) + elif msg["role"] == "assistant": + messages.append(AIMessage(content=msg["content"])) + + # Add current message + messages.append(HumanMessage(content=message)) + + # Use the react agent directly + result = await self.agent.ainvoke({"messages": messages}, {"recursion_limit": 6},) + + # Extract final response + final_response = "" + if "messages" in result and result["messages"]: + last_message = result["messages"][-1] + if hasattr(last_message, 'content'): + final_response = last_message.content + elif isinstance(last_message, dict) and "content" in last_message: + final_response = last_message["content"] + + return { + "response": final_response, + "intermediate_steps": [], + "success": True, + "error": None + } + + except Exception as e: + logger.error(f"LangGraph chat error: {str(e)}", exc_info=True) + return { + "response": f"抱歉,处理您的请求时出现错误: {str(e)}", + "intermediate_steps": [], + "success": False, + "error": str(e) + } + + async def chat_stream(self, message: str, chat_history: Optional[List[Dict[str, str]]] = None) -> AsyncGenerator[ + Dict[str, Any], None]: + """Process a chat message using LangGraph with streaming.""" + try: + logger.info(f"Starting streaming chat with message: {message[:100]}...") + + # Convert chat history to messages + messages = [] + if chat_history: + for msg in chat_history: + if msg["role"] == "user": + messages.append(HumanMessage(content=msg["content"])) + elif msg["role"] == "assistant": + messages.append(AIMessage(content=msg["content"])) + + # Add current message + messages.append(HumanMessage(content=message)) + + # Track state for streaming + intermediate_steps = [] + final_response_started = False + accumulated_response = "" + final_ai_message = None + + # Stream the agent execution + async for event in self.agent.astream({"messages": messages}): + # Handle different event types from LangGraph + print('event===', event) + if isinstance(event, dict): + for node_name, node_output in event.items(): + logger.info(f"Processing node: {node_name}, output type: {type(node_output)}") + + # 处理 tools 节点 + if "tools" in node_name.lower(): + # 提取工具信息 + tool_infos = [] + + if isinstance(node_output, dict) and "messages" in node_output: + messages_in_output = node_output["messages"] + + for msg in messages_in_output: + # 处理 ToolMessage 对象 + if hasattr(msg, 'name') and hasattr(msg, 'content'): + tool_info = { + "tool_name": msg.name, + "tool_output": msg.content, + "tool_call_id": getattr(msg, 'tool_call_id', ''), + "status": "completed" + } + tool_infos.append(tool_info) + elif isinstance(msg, dict): + if 'name' in msg and 'content' in msg: + tool_info = { + "tool_name": msg['name'], + "tool_output": msg['content'], + "tool_call_id": msg.get('tool_call_id', ''), + "status": "completed" + } + tool_infos.append(tool_info) + + # 返回 tools_end 事件 + for tool_info in tool_infos: + yield { + "type": "tools_end", + "content": f"工具 {tool_info['tool_name']} 执行完成", + "tool_name": tool_info["tool_name"], + "tool_output": tool_info["tool_output"], + "node_name": node_name, + "done": False + } + await asyncio.sleep(0.1) + + # 处理 agent 节点 + elif "agent" in node_name.lower(): + if isinstance(node_output, dict) and "messages" in node_output: + messages_in_output = node_output["messages"] + if messages_in_output: + last_msg = messages_in_output[-1] + + # 获取 finish_reason + finish_reason = None + if hasattr(last_msg, 'response_metadata'): + finish_reason = last_msg.response_metadata.get('finish_reason') + elif isinstance(last_msg, dict) and 'response_metadata' in last_msg: + finish_reason = last_msg['response_metadata'].get('finish_reason') + + # 判断是否为 thinking 或 response + if finish_reason == 'tool_calls': + # thinking 状态 + thinking_content = "🤔 正在思考..." + if hasattr(last_msg, 'content') and last_msg.content: + thinking_content = f"🤔 思考: {last_msg.content[:200]}..." + elif isinstance(last_msg, dict) and "content" in last_msg: + thinking_content = f"🤔 思考: {last_msg['content'][:200]}..." + + yield { + "type": "thinking", + "content": thinking_content, + "node_name": node_name, + "raw_output": str(node_output)[:500] if node_output else "", + "done": False + } + await asyncio.sleep(0.1) + + elif finish_reason == 'stop': + # response 状态 + if hasattr(last_msg, 'content') and hasattr(last_msg, + '__class__') and 'AI' in last_msg.__class__.__name__: + current_content = last_msg.content + final_ai_message = last_msg + + if not final_response_started and current_content: + final_response_started = True + yield { + "type": "response_start", + "content": "", + "intermediate_steps": intermediate_steps, + "done": False + } + + if current_content and len(current_content) > len(accumulated_response): + new_content = current_content[len(accumulated_response):] + + for char in new_content: + accumulated_response += char + yield { + "type": "response", + "content": accumulated_response, + "intermediate_steps": intermediate_steps, + "done": False + } + await asyncio.sleep(0.03) + + else: + # 其他 agent 状态 + yield { + "type": "step", + "content": f"📋 执行步骤: {node_name}", + "node_name": node_name, + "raw_output": str(node_output)[:500] if node_output else "", + "done": False + } + await asyncio.sleep(0.1) + + # 处理其他节点 + else: + yield { + "type": "step", + "content": f"📋 执行步骤: {node_name}", + "node_name": node_name, + "raw_output": str(node_output)[:500] if node_output else "", + "done": False + } + await asyncio.sleep(0.1) + + # 最终完成事件 + yield { + "type": "complete", + "content": accumulated_response, + "intermediate_steps": intermediate_steps, + "done": True + } + + except Exception as e: + logger.error(f"Error in chat_stream: {str(e)}", exc_info=True) + yield { + "type": "error", + "content": f"处理请求时出错: {str(e)}", + "done": True + } + + # 确保最终响应包含完整内容 + final_content = accumulated_response + if not final_content and final_ai_message and hasattr(final_ai_message, 'content'): + final_content = final_ai_message.content or "" + + # Final completion signal + yield { + "type": "response", + "content": final_content, + "intermediate_steps": intermediate_steps, + "done": True + } + + except Exception as e: + logger.error(f"LangGraph chat stream error: {str(e)}", exc_info=True) + yield { + "type": "error", + "content": f"抱歉,处理您的请求时出现错误: {str(e)}", + "error": str(e), + "done": True + } + + def get_available_tools(self) -> List[Dict[str, Any]]: + """Get list of available tools.""" + tools = [] + for tool in self.tools: + tools.append({ + "name": tool.name, + "description": tool.description, + "parameters": [], + "enabled": True + }) + return tools + + def get_config(self) -> Dict[str, Any]: + """Get current agent configuration.""" + return self.config.dict() + + def update_config(self, config: Dict[str, Any]): + """Update agent configuration.""" + for key, value in config.items(): + if hasattr(self.config, key): + setattr(self.config, key, value) + + # Recreate agent with new config + self._create_agent() + logger.info("Agent configuration updated") + + +# Global instance +_langgraph_agent_service: Optional[LangGraphAgentService] = None + + +def get_langgraph_agent_service(db_session=None) -> LangGraphAgentService: + """Get or create LangGraph agent service instance.""" + global _langgraph_agent_service + + if _langgraph_agent_service is None: + _langgraph_agent_service = LangGraphAgentService(db_session) + logger.info("LangGraph Agent service initialized") + + return _langgraph_agent_service \ No newline at end of file diff --git a/backend/th_agenter/services/agent_config.py b/backend/th_agenter/services/agent_config.py new file mode 100644 index 0000000..945bdbe --- /dev/null +++ b/backend/th_agenter/services/agent_config.py @@ -0,0 +1,206 @@ +"""Agent configuration service.""" + +from typing import List, Dict, Any, Optional +from sqlalchemy.orm import Session +from sqlalchemy import and_ + +from ..models.agent_config import AgentConfig +from ..utils.logger import get_logger +from ..utils.exceptions import ValidationError, NotFoundError + +logger = get_logger("agent_config_service") + + +class AgentConfigService: + """Service for managing agent configurations.""" + + def __init__(self, db: Session): + self.db = db + + def create_config(self, config_data: Dict[str, Any]) -> AgentConfig: + """Create a new agent configuration.""" + try: + # Validate required fields + if not config_data.get("name"): + raise ValidationError("Configuration name is required") + + # Check if name already exists + existing = self.db.query(AgentConfig).filter( + AgentConfig.name == config_data["name"] + ).first() + if existing: + raise ValidationError(f"Configuration with name '{config_data['name']}' already exists") + + # Create new configuration + config = AgentConfig( + name=config_data["name"], + description=config_data.get("description", ""), + enabled_tools=config_data.get("enabled_tools", ["calculator", "weather", "search", "datetime", "file"]), + max_iterations=config_data.get("max_iterations", 10), + temperature=config_data.get("temperature", 0.1), + system_message=config_data.get("system_message", "You are a helpful AI assistant with access to various tools. Use the available tools to help answer user questions accurately. Always explain your reasoning and the tools you're using."), + verbose=config_data.get("verbose", True), + is_active=config_data.get("is_active", True), + is_default=config_data.get("is_default", False) + ) + + # If this is set as default, unset other defaults + if config.is_default: + self.db.query(AgentConfig).filter( + AgentConfig.is_default == True + ).update({"is_default": False}) + + self.db.add(config) + self.db.commit() + self.db.refresh(config) + + logger.info(f"Created agent configuration: {config.name}") + return config + + except Exception as e: + self.db.rollback() + logger.error(f"Error creating agent configuration: {str(e)}") + raise + + def get_config(self, config_id: int) -> Optional[AgentConfig]: + """Get agent configuration by ID.""" + return self.db.query(AgentConfig).filter( + AgentConfig.id == config_id + ).first() + + def get_config_by_name(self, name: str) -> Optional[AgentConfig]: + """Get agent configuration by name.""" + return self.db.query(AgentConfig).filter( + AgentConfig.name == name + ).first() + + def get_default_config(self) -> Optional[AgentConfig]: + """Get default agent configuration.""" + return self.db.query(AgentConfig).filter( + and_(AgentConfig.is_default == True, AgentConfig.is_active == True) + ).first() + + def list_configs(self, active_only: bool = True) -> List[AgentConfig]: + """List all agent configurations.""" + query = self.db.query(AgentConfig) + if active_only: + query = query.filter(AgentConfig.is_active == True) + return query.order_by(AgentConfig.created_at.desc()).all() + + def update_config(self, config_id: int, config_data: Dict[str, Any]) -> AgentConfig: + """Update agent configuration.""" + try: + config = self.get_config(config_id) + if not config: + raise NotFoundError(f"Agent configuration with ID {config_id} not found") + + # Check if name change conflicts with existing + if "name" in config_data and config_data["name"] != config.name: + existing = self.db.query(AgentConfig).filter( + and_( + AgentConfig.name == config_data["name"], + AgentConfig.id != config_id + ) + ).first() + if existing: + raise ValidationError(f"Configuration with name '{config_data['name']}' already exists") + + # Update fields + for key, value in config_data.items(): + if hasattr(config, key): + setattr(config, key, value) + + # If this is set as default, unset other defaults + if config_data.get("is_default", False): + self.db.query(AgentConfig).filter( + and_( + AgentConfig.is_default == True, + AgentConfig.id != config_id + ) + ).update({"is_default": False}) + + self.db.commit() + self.db.refresh(config) + + logger.info(f"Updated agent configuration: {config.name}") + return config + + except Exception as e: + self.db.rollback() + logger.error(f"Error updating agent configuration: {str(e)}") + raise + + def delete_config(self, config_id: int) -> bool: + """Delete agent configuration (soft delete by setting is_active=False).""" + try: + config = self.get_config(config_id) + if not config: + raise NotFoundError(f"Agent configuration with ID {config_id} not found") + + # Don't allow deleting the default configuration + if config.is_default: + raise ValidationError("Cannot delete the default configuration") + + config.is_active = False + self.db.commit() + + logger.info(f"Deleted agent configuration: {config.name}") + return True + + except Exception as e: + self.db.rollback() + logger.error(f"Error deleting agent configuration: {str(e)}") + raise + + def set_default_config(self, config_id: int) -> AgentConfig: + """Set a configuration as default.""" + try: + config = self.get_config(config_id) + if not config: + raise NotFoundError(f"Agent configuration with ID {config_id} not found") + + if not config.is_active: + raise ValidationError("Cannot set inactive configuration as default") + + # Unset other defaults + self.db.query(AgentConfig).filter( + AgentConfig.is_default == True + ).update({"is_default": False}) + + # Set this as default + config.is_default = True + self.db.commit() + self.db.refresh(config) + + logger.info(f"Set default agent configuration: {config.name}") + return config + + except Exception as e: + self.db.rollback() + logger.error(f"Error setting default agent configuration: {str(e)}") + raise + + def get_config_dict(self, config_id: Optional[int] = None) -> Dict[str, Any]: + """Get configuration as dictionary. If no ID provided, returns default config.""" + if config_id: + config = self.get_config(config_id) + else: + config = self.get_default_config() + + if not config: + # Return default values if no configuration found + return { + "enabled_tools": ["calculator", "weather", "search", "datetime", "file", "generate_image"], + "max_iterations": 10, + "temperature": 0.1, + "system_message": "You are a helpful AI assistant with access to various tools. Use the available tools to help answer user questions accurately. Always explain your reasoning and the tools you're using.", + "verbose": True + } + + return { + "enabled_tools": config.enabled_tools, + "max_iterations": config.max_iterations, + "temperature": config.temperature, + "system_message": config.system_message, + "verbose": config.verbose + } \ No newline at end of file diff --git a/backend/th_agenter/services/auth.py b/backend/th_agenter/services/auth.py new file mode 100644 index 0000000..fc77f3e --- /dev/null +++ b/backend/th_agenter/services/auth.py @@ -0,0 +1,141 @@ +"""Authentication service.""" + +from typing import Optional +from datetime import datetime, timedelta +from fastapi import Depends, HTTPException, status +from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials +from sqlalchemy.orm import Session +import jwt +import bcrypt + +from ..core.config import settings +from ..db.database import get_db +from ..models.user import User + + +security = HTTPBearer() + + +class AuthService: + """Authentication service.""" + + @staticmethod + def verify_password(plain_password: str, hashed_password: str) -> bool: + """Verify a password against its hash.""" + # Bcrypt has a maximum password length of 72 bytes + plain_password = plain_password[:72] + return bcrypt.checkpw(plain_password.encode('utf-8'), hashed_password.encode('utf-8')) + + @staticmethod + def get_password_hash(password: str) -> str: + """Generate password hash.""" + # Bcrypt has a maximum password length of 72 bytes + password = password[:72] + salt = bcrypt.gensalt() + hashed = bcrypt.hashpw(password.encode('utf-8'), salt) + return hashed.decode('utf-8') + + @staticmethod + def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + """Create JWT access token.""" + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=settings.security.access_token_expire_minutes) + + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode( + to_encode, + settings.security.secret_key, + algorithm=settings.security.algorithm + ) + return encoded_jwt + + @staticmethod + def verify_token(token: str) -> Optional[dict]: + """Verify JWT token.""" + try: + payload = jwt.decode( + token, + settings.security.secret_key, + algorithms=[settings.security.algorithm] + ) + return payload + except jwt.PyJWTError as e: + import logging + logging.error(f"Token verification failed: {e}") + logging.error(f"Token: {token[:50]}...") + logging.error(f"Secret key: {settings.security.secret_key[:20]}...") + logging.error(f"Algorithm: {settings.security.algorithm}") + return None + + @staticmethod + def authenticate_user(db: Session, username: str, password: str) -> Optional[User]: + """Authenticate user with username and password.""" + user = db.query(User).filter(User.username == username).first() + if not user: + return None + if not AuthService.verify_password(password, user.hashed_password): + return None + return user + + @staticmethod + def authenticate_user_by_email(db: Session, email: str, password: str) -> Optional[User]: + """Authenticate user with email and password.""" + user = db.query(User).filter(User.email == email).first() + if not user: + return None + if not AuthService.verify_password(password, user.hashed_password): + return None + return user + + @staticmethod + def get_current_user( + credentials: HTTPAuthorizationCredentials = Depends(security), + db: Session = Depends(get_db) + ) -> User: + """Get current authenticated user.""" + import logging + from ..core.context import UserContext + + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + + token = credentials.credentials + logging.info(f"Received token: {token[:50]}...") + payload = AuthService.verify_token(token) + if payload is None: + logging.error("Token verification failed") + raise credentials_exception + + logging.info(f"Token payload: {payload}") + username: str = payload.get("sub") + if username is None: + logging.error("No username in token payload") + raise credentials_exception + + logging.info(f"Looking for user with username: {username}") + user = db.query(User).filter(User.username == username).first() + if user is None: + logging.error(f"User not found with username: {username}") + raise credentials_exception + + # Set user in context for global access + UserContext.set_current_user(user) + logging.info(f"User {user.username} (ID: {user.id}) set in UserContext") + + return user + + @staticmethod + def get_current_active_user(current_user: User = Depends(get_current_user)) -> User: + """Get current active user.""" + if not current_user.is_active: + raise HTTPException( + status_code=status.HTTP_400_BAD_REQUEST, + detail="Inactive user" + ) + return current_user \ No newline at end of file diff --git a/backend/th_agenter/services/chat.py b/backend/th_agenter/services/chat.py new file mode 100644 index 0000000..040e26c --- /dev/null +++ b/backend/th_agenter/services/chat.py @@ -0,0 +1,363 @@ +"""Chat service for AI model integration using LangChain.""" + +import json +import asyncio +import os +from typing import AsyncGenerator, Optional, List, Dict, Any +from sqlalchemy.orm import Session + +from ..core.config import settings +from ..models.message import MessageRole +from ..utils.schemas import ChatResponse, StreamChunk, MessageResponse +from ..utils.exceptions import ChatServiceError, OpenAIError +from ..utils.logger import get_logger +from .conversation import ConversationService +from .langchain_chat import LangChainChatService +from .knowledge_chat import KnowledgeChatService +from .agent.agent_service import get_agent_service +from .agent.langgraph_agent_service import get_langgraph_agent_service + +logger = get_logger("chat_service") + + +class ChatService: + """Service for handling AI chat functionality using LangChain.""" + + def __init__(self, db: Session): + self.db = db + self.conversation_service = ConversationService(db) + + # Initialize LangChain chat service + self.langchain_service = LangChainChatService(db) + + # Initialize Knowledge chat service with exception handling + try: + self.knowledge_service = KnowledgeChatService(db) + except Exception as e: + logger.warning(f"Failed to initialize KnowledgeChatService: {str(e)}. Knowledge base features will be disabled.") + self.knowledge_service = None + + # Initialize Agent service with database session + try: + self.agent_service = get_agent_service(db) + if not self.agent_service: + logger.warning("AgentService is not available. Agent functionality will be disabled.") + except Exception as e: + logger.warning(f"Failed to initialize AgentService: {str(e)}. Agent functionality will be disabled.") + self.agent_service = None + + # Initialize LangGraph Agent service with database session + try: + self.langgraph_service = get_langgraph_agent_service(db) + if not self.langgraph_service: + logger.warning("LangGraphAgentService is not available. LangGraph functionality will be disabled.") + except Exception as e: + logger.warning(f"Failed to initialize LangGraphAgentService: {str(e)}. LangGraph functionality will be disabled.") + self.langgraph_service = None + + logger.info("ChatService initialized with LangChain backend and Agent support") + + + + async def chat( + self, + conversation_id: int, + message: str, + stream: bool = False, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + use_agent: bool = False, + use_langgraph: bool = False, + use_knowledge_base: bool = False, + knowledge_base_id: Optional[int] = None + ) -> ChatResponse: + """Send a message and get AI response using LangChain, Agent, or Knowledge Base.""" + if use_knowledge_base and knowledge_base_id: + if not self.knowledge_service: + raise ChatServiceError("Knowledge base features are unavailable due to initialization error.") + + logger.info(f"Processing chat request for conversation {conversation_id} via Knowledge Base {knowledge_base_id}") + + # Use knowledge base chat service + return await self.knowledge_service.chat_with_knowledge_base( + conversation_id=conversation_id, + message=message, + knowledge_base_id=knowledge_base_id, + stream=stream, + temperature=temperature, + max_tokens=max_tokens + ) + elif use_langgraph: + if not self.langgraph_service: + raise ChatServiceError("LangGraph agent features are unavailable due to initialization error.") + + logger.info(f"Processing chat request for conversation {conversation_id} via LangGraph Agent") + + # Get conversation history for LangGraph agent + conversation = self.conversation_service.get_conversation(conversation_id) + if not conversation: + raise ChatServiceError(f"Conversation {conversation_id} not found") + + messages = self.conversation_service.get_conversation_messages(conversation_id) + chat_history = [{ + "role": "user" if msg.role == MessageRole.USER else "assistant", + "content": msg.content + } for msg in messages] + + # Use LangGraph agent service + agent_result = await self.langgraph_service.chat(message, chat_history) + + if agent_result["success"]: + # Save user message + user_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=message, + role=MessageRole.USER + ) + + # Save assistant response + assistant_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=agent_result["response"], + role=MessageRole.ASSISTANT, + message_metadata={"intermediate_steps": agent_result["intermediate_steps"]} + ) + + return ChatResponse( + message=MessageResponse( + id=assistant_message.id, + content=agent_result["response"], + role=MessageRole.ASSISTANT, + conversation_id=conversation_id, + created_at=assistant_message.created_at, + metadata=assistant_message.metadata + ) + ) + else: + raise ChatServiceError(f"LangGraph Agent error: {agent_result.get('error', 'Unknown error')}") + elif use_agent: + if not self.agent_service: + raise ChatServiceError("Agent features are unavailable due to initialization error.") + + logger.info(f"Processing chat request for conversation {conversation_id} via Agent") + + # Get conversation history for agent + conversation = self.conversation_service.get_conversation(conversation_id) + if not conversation: + raise ChatServiceError(f"Conversation {conversation_id} not found") + + messages = self.conversation_service.get_conversation_messages(conversation_id) + chat_history = [{ + "role": "user" if msg.role == MessageRole.USER else "assistant", + "content": msg.content + } for msg in messages] + + # Use agent service + agent_result = await self.agent_service.chat(message, chat_history) + + if agent_result["success"]: + # Save user message + user_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=message, + role=MessageRole.USER + ) + + # Save assistant response + assistant_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=agent_result["response"], + role=MessageRole.ASSISTANT, + message_metadata={"tool_calls": agent_result["tool_calls"]} + ) + + return ChatResponse( + message=MessageResponse( + id=assistant_message.id, + content=agent_result["response"], + role=MessageRole.ASSISTANT, + conversation_id=conversation_id, + created_at=assistant_message.created_at, + metadata=assistant_message.metadata + ) + ) + else: + raise ChatServiceError(f"Agent error: {agent_result.get('error', 'Unknown error')}") + else: + logger.info(f"Processing chat request for conversation {conversation_id} via LangChain") + + # Delegate to LangChain service + return await self.langchain_service.chat( + conversation_id=conversation_id, + message=message, + stream=stream, + temperature=temperature, + max_tokens=max_tokens + ) + + async def chat_stream( + self, + conversation_id: int, + message: str, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None, + use_agent: bool = False, + use_langgraph: bool = False, + use_knowledge_base: bool = False, + knowledge_base_id: Optional[int] = None + ) -> AsyncGenerator[str, None]: + """Send a message and get streaming AI response using LangChain, Agent, or Knowledge Base.""" + if use_knowledge_base and knowledge_base_id: + logger.info(f"Processing streaming chat request for conversation {conversation_id} via Knowledge Base {knowledge_base_id}") + + # Use knowledge base chat service streaming + async for content in self.knowledge_service.chat_stream_with_knowledge_base( + conversation_id=conversation_id, + message=message, + knowledge_base_id=knowledge_base_id, + temperature=temperature, + max_tokens=max_tokens + ): + # Create stream chunk for compatibility with existing API + stream_chunk = StreamChunk( + content=content, + role=MessageRole.ASSISTANT + ) + yield json.dumps(stream_chunk.dict(), ensure_ascii=False) + elif use_langgraph: + logger.info(f"Processing streaming chat request for conversation {conversation_id} via LangGraph Agent") + + # Get conversation history for LangGraph agent + conversation = self.conversation_service.get_conversation(conversation_id) + if not conversation: + raise ChatServiceError(f"Conversation {conversation_id} not found") + + messages = self.conversation_service.get_conversation_messages(conversation_id) + chat_history = [{ + "role": "user" if msg.role == MessageRole.USER else "assistant", + "content": msg.content + } for msg in messages] + + # Save user message first + user_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=message, + role=MessageRole.USER + ) + + # Use LangGraph agent service streaming + full_response = "" + intermediate_steps = [] + + async for chunk in self.langgraph_service.chat_stream(message, chat_history): + if chunk["type"] == "response": + full_response = chunk["content"] + intermediate_steps = chunk.get("intermediate_steps", []) + + # Return the chunk as-is to maintain type information + yield json.dumps(chunk, ensure_ascii=False) + + elif chunk["type"] == "error": + # Return the chunk as-is to maintain type information + yield json.dumps(chunk, ensure_ascii=False) + return + else: + # For other types (status, step, etc.), pass through + yield json.dumps(chunk, ensure_ascii=False) + + # Save assistant response + if full_response: + self.conversation_service.add_message( + conversation_id=conversation_id, + content=full_response, + role=MessageRole.ASSISTANT, + message_metadata={"intermediate_steps": intermediate_steps} + ) + elif use_agent: + logger.info(f"Processing streaming chat request for conversation {conversation_id} via Agent") + + # Get conversation history for agent + conversation = self.conversation_service.get_conversation(conversation_id) + if not conversation: + raise ChatServiceError(f"Conversation {conversation_id} not found") + + messages = self.conversation_service.get_conversation_messages(conversation_id) + chat_history = [{ + "role": "user" if msg.role == MessageRole.USER else "assistant", + "content": msg.content + } for msg in messages] + + # Save user message first + user_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=message, + role=MessageRole.USER + ) + + # Use agent service streaming + full_response = "" + tool_calls = [] + + async for chunk in self.agent_service.chat_stream(message, chat_history): + if chunk["type"] == "response": + full_response = chunk["content"] + tool_calls = chunk.get("tool_calls", []) + + # Return the chunk as-is to maintain type information + yield json.dumps(chunk, ensure_ascii=False) + + elif chunk["type"] == "error": + # Return the chunk as-is to maintain type information + yield json.dumps(chunk, ensure_ascii=False) + return + else: + # For other types (status, tool_start, etc.), pass through + yield json.dumps(chunk, ensure_ascii=False) + + # Save assistant response + if full_response: + self.conversation_service.add_message( + conversation_id=conversation_id, + content=full_response, + role=MessageRole.ASSISTANT, + message_metadata={"tool_calls": tool_calls} + ) + else: + logger.info(f"Processing streaming chat request for conversation {conversation_id} via LangChain") + + # Delegate to LangChain service and wrap response in JSON format + async for content in self.langchain_service.chat_stream( + conversation_id=conversation_id, + message=message, + temperature=temperature, + max_tokens=max_tokens + ): + # Create stream chunk for compatibility with existing API + stream_chunk = StreamChunk( + content=content, + role=MessageRole.ASSISTANT + ) + yield json.dumps(stream_chunk.dict(), ensure_ascii=False) + + async def get_available_models(self) -> List[str]: + """Get list of available models from LangChain.""" + logger.info("Getting available models via LangChain") + + # Delegate to LangChain service + return await self.langchain_service.get_available_models() + + def update_model_config( + self, + model: Optional[str] = None, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None + ): + """Update LLM configuration via LangChain.""" + logger.info(f"Updating model config via LangChain: model={model}, temperature={temperature}, max_tokens={max_tokens}") + + # Delegate to LangChain service + self.langchain_service.update_model_config( + model=model, + temperature=temperature, + max_tokens=max_tokens + ) \ No newline at end of file diff --git a/backend/th_agenter/services/conversation.py b/backend/th_agenter/services/conversation.py new file mode 100644 index 0000000..f69ca00 --- /dev/null +++ b/backend/th_agenter/services/conversation.py @@ -0,0 +1,260 @@ +"""Conversation service.""" + +from typing import List, Optional +from sqlalchemy.orm import Session +from sqlalchemy import desc, func, or_ + +from ..models.conversation import Conversation +from ..models.message import Message, MessageRole +from ..utils.schemas import ConversationCreate, ConversationUpdate +from ..utils.exceptions import ConversationNotFoundError, DatabaseError +from ..utils.logger import get_logger +from ..core.context import UserContext + +logger = get_logger("conversation_service") + + +class ConversationService: + """Service for managing conversations and messages.""" + + def __init__(self, db: Session): + self.db = db + + def create_conversation( + self, + user_id: int, + conversation_data: ConversationCreate + ) -> Conversation: + """Create a new conversation.""" + logger.info(f"Creating new conversation for user {user_id}") + + try: + conversation = Conversation( + **conversation_data.dict(), + user_id=user_id + ) + + # Set audit fields + conversation.set_audit_fields(user_id=user_id, is_update=False) + + self.db.add(conversation) + self.db.commit() + self.db.refresh(conversation) + + logger.info(f"Successfully created conversation {conversation.id} for user {user_id}") + return conversation + + except Exception as e: + logger.error(f"Failed to create conversation: {str(e)}", exc_info=True) + self.db.rollback() + raise DatabaseError(f"Failed to create conversation: {str(e)}") + + def get_conversation(self, conversation_id: int) -> Optional[Conversation]: + """Get a conversation by ID.""" + try: + user_id = UserContext.get_current_user_id() + conversation = self.db.query(Conversation).filter( + Conversation.id == conversation_id, + Conversation.user_id == user_id + ).first() + + if not conversation: + logger.warning(f"Conversation {conversation_id} not found") + + return conversation + + except Exception as e: + logger.error(f"Failed to get conversation {conversation_id}: {str(e)}", exc_info=True) + raise DatabaseError(f"Failed to get conversation: {str(e)}") + + def get_user_conversations( + self, + skip: int = 0, + limit: int = 50, + search_query: Optional[str] = None, + include_archived: bool = False, + order_by: str = "updated_at", + order_desc: bool = True + ) -> List[Conversation]: + """Get user's conversations with search and filtering.""" + user_id = UserContext.get_current_user_id() + query = self.db.query(Conversation).filter( + Conversation.user_id == user_id + ) + + # Filter archived conversations + if not include_archived: + query = query.filter(Conversation.is_archived == False) + + # Search functionality + if search_query and search_query.strip(): + search_term = f"%{search_query.strip()}%" + query = query.filter( + or_( + Conversation.title.ilike(search_term), + Conversation.system_prompt.ilike(search_term) + ) + ) + + # Ordering + order_column = getattr(Conversation, order_by, Conversation.updated_at) + if order_desc: + query = query.order_by(desc(order_column)) + else: + query = query.order_by(order_column) + + return query.offset(skip).limit(limit).all() + + def update_conversation( + self, + conversation_id: int, + conversation_update: ConversationUpdate + ) -> Optional[Conversation]: + """Update a conversation.""" + conversation = self.get_conversation(conversation_id) + if not conversation: + return None + + update_data = conversation_update.dict(exclude_unset=True) + for field, value in update_data.items(): + setattr(conversation, field, value) + + self.db.commit() + self.db.refresh(conversation) + return conversation + + def delete_conversation(self, conversation_id: int) -> bool: + """Delete a conversation.""" + conversation = self.get_conversation(conversation_id) + if not conversation: + return False + + self.db.delete(conversation) + self.db.commit() + return True + + def get_conversation_messages( + self, + conversation_id: int, + skip: int = 0, + limit: int = 100 + ) -> List[Message]: + """Get messages from a conversation.""" + return self.db.query(Message).filter( + Message.conversation_id == conversation_id + ).order_by(Message.created_at).offset(skip).limit(limit).all() + + def add_message( + self, + conversation_id: int, + content: str, + role: MessageRole, + message_metadata: Optional[dict] = None, + context_documents: Optional[list] = None, + prompt_tokens: Optional[int] = None, + completion_tokens: Optional[int] = None, + total_tokens: Optional[int] = None + ) -> Message: + """Add a message to a conversation.""" + message = Message( + conversation_id=conversation_id, + content=content, + role=role, + message_metadata=message_metadata, + context_documents=context_documents, + prompt_tokens=prompt_tokens, + completion_tokens=completion_tokens, + total_tokens=total_tokens + ) + + # Set audit fields + message.set_audit_fields() + + self.db.add(message) + self.db.commit() + self.db.refresh(message) + return message + + def get_conversation_history( + self, + conversation_id: int, + limit: int = 20 + ) -> List[Message]: + """Get recent conversation history for context.""" + return self.db.query(Message).filter( + Message.conversation_id == conversation_id + ).order_by(desc(Message.created_at)).limit(limit).all()[::-1] # Reverse to get chronological order + + def update_conversation_timestamp(self, conversation_id: int) -> None: + """Update conversation's updated_at timestamp.""" + conversation = self.get_conversation(conversation_id) + if conversation: + # SQLAlchemy will automatically update the updated_at field + self.db.commit() + + def get_user_conversations_count( + self, + search_query: Optional[str] = None, + include_archived: bool = False + ) -> int: + """Get total count of user's conversations.""" + user_id = UserContext.get_current_user_id() + query = self.db.query(func.count(Conversation.id)).filter( + Conversation.user_id == user_id + ) + + if not include_archived: + query = query.filter(Conversation.is_archived == False) + + if search_query and search_query.strip(): + search_term = f"%{search_query.strip()}%" + query = query.filter( + or_( + Conversation.title.ilike(search_term), + Conversation.system_prompt.ilike(search_term) + ) + ) + + return query.scalar() or 0 + + def archive_conversation(self, conversation_id: int) -> bool: + """Archive a conversation.""" + conversation = self.get_conversation(conversation_id) + if not conversation: + return False + + conversation.is_archived = True + self.db.commit() + return True + + def unarchive_conversation(self, conversation_id: int) -> bool: + """Unarchive a conversation.""" + conversation = self.get_conversation(conversation_id) + if not conversation: + return False + + conversation.is_archived = False + self.db.commit() + return True + + def delete_all_conversations(self) -> bool: + """Delete all conversations for the current user.""" + try: + user_id = UserContext.get_current_user_id() + # Get all conversations for the user + conversations = self.db.query(Conversation).filter( + Conversation.user_id == user_id + ).all() + + # Delete each conversation + for conversation in conversations: + self.db.delete(conversation) + + self.db.commit() + logger.info(f"Successfully deleted all conversations for user {user_id}") + return True + + except Exception as e: + logger.error(f"Failed to delete all conversations: {str(e)}", exc_info=True) + self.db.rollback() + raise DatabaseError(f"Failed to delete all conversations: {str(e)}") \ No newline at end of file diff --git a/backend/th_agenter/services/conversation_context.py b/backend/th_agenter/services/conversation_context.py new file mode 100644 index 0000000..dc13d3c --- /dev/null +++ b/backend/th_agenter/services/conversation_context.py @@ -0,0 +1,310 @@ +from typing import Dict, Any, List, Optional +import json +from datetime import datetime +from sqlalchemy.orm import Session +from th_agenter.models.conversation import Conversation +from th_agenter.models.message import Message +from th_agenter.db.database import get_db + +class ConversationContextService: + """ + 对话上下文管理服务 + 用于管理智能问数的对话历史和上下文信息 + """ + + def __init__(self): + self.context_cache = {} # 内存缓存对话上下文 + + async def create_conversation(self, user_id: int, title: str = "智能问数对话") -> int: + """ + 创建新的对话 + + Args: + user_id: 用户ID + title: 对话标题 + + Returns: + 新创建的对话ID + """ + try: + db = next(get_db()) + + conversation = Conversation( + user_id=user_id, + title=title, + created_at=datetime.utcnow(), + updated_at=datetime.utcnow() + ) + + db.add(conversation) + db.commit() + db.refresh(conversation) + + # 初始化对话上下文 + self.context_cache[conversation.id] = { + 'conversation_id': conversation.id, + 'user_id': user_id, + 'file_list': [], + 'selected_files': [], + 'query_history': [], + 'created_at': datetime.utcnow().isoformat() + } + + return conversation.id + + except Exception as e: + print(f"创建对话失败: {e}") + raise + finally: + db.close() + + async def get_conversation_context(self, conversation_id: int) -> Optional[Dict[str, Any]]: + """ + 获取对话上下文 + + Args: + conversation_id: 对话ID + + Returns: + 对话上下文信息 + """ + # 先从缓存中查找 + if conversation_id in self.context_cache: + return self.context_cache[conversation_id] + + # 从数据库加载 + try: + db = next(get_db()) + + conversation = db.query(Conversation).filter( + Conversation.id == conversation_id + ).first() + + if not conversation: + return None + + # 加载消息历史 + messages = db.query(Message).filter( + Message.conversation_id == conversation_id + ).order_by(Message.created_at).all() + + # 重建上下文 + context = { + 'conversation_id': conversation_id, + 'user_id': conversation.user_id, + 'file_list': [], + 'selected_files': [], + 'query_history': [], + 'created_at': conversation.created_at.isoformat() + } + + # 从消息中提取查询历史 + for message in messages: + if message.role == 'user': + context['query_history'].append({ + 'query': message.content, + 'timestamp': message.created_at.isoformat() + }) + elif message.role == 'assistant' and message.metadata: + # 从助手消息的元数据中提取文件信息 + try: + metadata = json.loads(message.metadata) if isinstance(message.metadata, str) else message.metadata + if 'selected_files' in metadata: + context['selected_files'] = metadata['selected_files'] + if 'file_list' in metadata: + context['file_list'] = metadata['file_list'] + except (json.JSONDecodeError, TypeError): + pass + + # 缓存上下文 + self.context_cache[conversation_id] = context + + return context + + except Exception as e: + print(f"获取对话上下文失败: {e}") + return None + finally: + db.close() + + async def update_conversation_context( + self, + conversation_id: int, + file_list: List[Dict[str, Any]] = None, + selected_files: List[Dict[str, Any]] = None, + query: str = None + ) -> bool: + """ + 更新对话上下文 + + Args: + conversation_id: 对话ID + file_list: 文件列表 + selected_files: 选中的文件 + query: 用户查询 + + Returns: + 更新是否成功 + """ + try: + # 获取或创建上下文 + context = await self.get_conversation_context(conversation_id) + if not context: + return False + + # 更新上下文信息 + if file_list is not None: + context['file_list'] = file_list + + if selected_files is not None: + context['selected_files'] = selected_files + + if query is not None: + context['query_history'].append({ + 'query': query, + 'timestamp': datetime.utcnow().isoformat() + }) + + # 更新缓存 + self.context_cache[conversation_id] = context + + return True + + except Exception as e: + print(f"更新对话上下文失败: {e}") + return False + + async def save_message( + self, + conversation_id: int, + role: str, + content: str, + metadata: Dict[str, Any] = None + ) -> bool: + """ + 保存消息到数据库 + + Args: + conversation_id: 对话ID + role: 消息角色 (user/assistant) + content: 消息内容 + metadata: 元数据 + + Returns: + 保存是否成功 + """ + try: + db = next(get_db()) + + message = Message( + conversation_id=conversation_id, + role=role, + content=content, + metadata=json.dumps(metadata) if metadata else None, + created_at=datetime.utcnow() + ) + + db.add(message) + db.commit() + + # 更新对话的最后更新时间 + conversation = db.query(Conversation).filter( + Conversation.id == conversation_id + ).first() + + if conversation: + conversation.updated_at = datetime.utcnow() + db.commit() + + return True + + except Exception as e: + print(f"保存消息失败: {e}") + return False + finally: + db.close() + + async def reset_conversation_context(self, conversation_id: int) -> bool: + """ + 重置对话上下文 + + Args: + conversation_id: 对话ID + + Returns: + 重置是否成功 + """ + try: + # 清除缓存 + if conversation_id in self.context_cache: + context = self.context_cache[conversation_id] + # 保留基本信息,清除文件和查询历史 + context.update({ + 'file_list': [], + 'selected_files': [], + 'query_history': [] + }) + + return True + + except Exception as e: + print(f"重置对话上下文失败: {e}") + return False + + async def get_conversation_history(self, conversation_id: int) -> List[Dict[str, Any]]: + """ + 获取对话历史消息 + + Args: + conversation_id: 对话ID + + Returns: + 消息历史列表 + """ + try: + db = next(get_db()) + + messages = db.query(Message).filter( + Message.conversation_id == conversation_id + ).order_by(Message.created_at).all() + + history = [] + for message in messages: + msg_data = { + 'id': message.id, + 'role': message.role, + 'content': message.content, + 'timestamp': message.created_at.isoformat() + } + + if message.metadata: + try: + metadata = json.loads(message.metadata) if isinstance(message.metadata, str) else message.metadata + msg_data['metadata'] = metadata + except (json.JSONDecodeError, TypeError): + pass + + history.append(msg_data) + + return history + + except Exception as e: + print(f"获取对话历史失败: {e}") + return [] + finally: + db.close() + + def clear_cache(self, conversation_id: int = None): + """ + 清除缓存 + + Args: + conversation_id: 特定对话ID,如果为None则清除所有缓存 + """ + if conversation_id: + self.context_cache.pop(conversation_id, None) + else: + self.context_cache.clear() + +# 全局实例 +conversation_context_service = ConversationContextService() \ No newline at end of file diff --git a/backend/th_agenter/services/database_config_service.py b/backend/th_agenter/services/database_config_service.py new file mode 100644 index 0000000..b2d8155 --- /dev/null +++ b/backend/th_agenter/services/database_config_service.py @@ -0,0 +1,324 @@ +"""数据库配置服务""" + +import json +from typing import List, Dict, Any, Optional +from sqlalchemy.orm import Session +from sqlalchemy import and_ +from cryptography.fernet import Fernet +import base64 +import os + +from ..models.database_config import DatabaseConfig +from ..utils.logger import get_logger +from ..utils.exceptions import ValidationError, NotFoundError +from .postgresql_tool_manager import get_postgresql_tool +from .mysql_tool_manager import get_mysql_tool + +# Try to import pymysql for MySQL support +try: + import pymysql + PYMYSQL_AVAILABLE = True +except ImportError: + pymysql = None + PYMYSQL_AVAILABLE = False + +logger = get_logger("database_config_service") + + +class DatabaseConfigService: + """数据库配置管理服务""" + + def __init__(self, db_session: Session): + self.db = db_session + self.postgresql_tool = get_postgresql_tool() + self.mysql_tool = get_mysql_tool() + # 初始化加密密钥 + self.encryption_key = self._get_or_create_encryption_key() + self.cipher = Fernet(self.encryption_key) + def _get_or_create_encryption_key(self) -> bytes: + """获取或创建加密密钥""" + key_file = "db/db_config_key.key" + if os.path.exists(key_file): + print('find db_config_key') + with open(key_file, 'rb') as f: + return f.read() + + else: + print('not find db_config_key') + key = Fernet.generate_key() + with open(key_file, 'wb') as f: + f.write(key) + return key + + def _encrypt_password(self, password: str) -> str: + """加密密码""" + return self.cipher.encrypt(password.encode()).decode() + + def _decrypt_password(self, encrypted_password: str) -> str: + """解密密码""" + return self.cipher.decrypt(encrypted_password.encode()).decode() + + async def create_config(self, user_id: int, config_data: Dict[str, Any]) -> DatabaseConfig: + """创建数据库配置""" + try: + # 验证配置 + required_fields = ['name', 'db_type', 'host', 'port', 'database', 'username', 'password'] + for field in required_fields: + if field not in config_data: + raise ValidationError(f"缺少必需字段: {field}") + + + # 测试连接 + test_config = { + 'host': config_data['host'], + 'port': config_data['port'], + 'database': config_data['database'], + 'username': config_data['username'], + 'password': config_data['password'] + } + if 'postgresql' == config_data['db_type']: + test_result = await self.postgresql_tool.execute( + operation="test_connection", + connection_config=test_config + ) + if not test_result.success: + raise ValidationError(f"数据库连接测试失败: {test_result.error}") + elif 'mysql' == config_data['db_type']: + test_result = await self.mysql_tool.execute( + operation="test_connection", + connection_config=test_config + ) + if not test_result.success: + raise ValidationError(f"数据库连接测试失败: {test_result.error}") + # 如果设置为默认配置,先取消其他默认配置 + if config_data.get('is_default', False): + self.db.query(DatabaseConfig).filter( + and_(DatabaseConfig.created_by == user_id, DatabaseConfig.is_default == True) + ).update({'is_default': False}) + + # 创建配置 + db_config = DatabaseConfig( + created_by=user_id, + name=config_data['name'], + db_type=config_data['db_type'], + host=config_data['host'], + port=config_data['port'], + database=config_data['database'], + username=config_data['username'], + password=self._encrypt_password(config_data['password']), + is_active=config_data.get('is_active', True), + is_default=config_data.get('is_default', False), + connection_params=config_data.get('connection_params') + ) + + self.db.add(db_config) + self.db.commit() + self.db.refresh(db_config) + + logger.info(f"创建数据库配置成功: {db_config.name} (ID: {db_config.id})") + return db_config + + except Exception as e: + self.db.rollback() + logger.error(f"创建数据库配置失败: {str(e)}") + raise + + def get_user_configs(self, user_id: int, active_only: bool = True) -> List[DatabaseConfig]: + """获取用户的数据库配置列表""" + query = self.db.query(DatabaseConfig).filter(DatabaseConfig.created_by == user_id) + if active_only: + query = query.filter(DatabaseConfig.is_active == True) + return query.order_by(DatabaseConfig.created_at.desc()).all() + + def get_config_by_id(self, config_id: int, user_id: int) -> Optional[DatabaseConfig]: + """根据ID获取配置""" + return self.db.query(DatabaseConfig).filter( + and_(DatabaseConfig.id == config_id, DatabaseConfig.created_by == user_id) + ).first() + + def get_default_config(self, user_id: int) -> Optional[DatabaseConfig]: + """获取用户的默认配置""" + return self.db.query(DatabaseConfig).filter( + and_( + DatabaseConfig.created_by == user_id, + # DatabaseConfig.is_default == True, + DatabaseConfig.is_active == True + ) + ).first() + + async def test_connection(self, config_id: int, user_id: int) -> Dict[str, Any]: + """测试数据库连接""" + config = self.get_config_by_id(config_id, user_id) + if not config: + raise NotFoundError("数据库配置不存在") + + test_config = { + 'host': config.host, + 'port': config.port, + 'database': config.database, + 'username': config.username, + 'password': self._decrypt_password(config.password) + } + + result = await self.postgresql_tool.execute( + operation="test_connection", + connection_config=test_config + ) + + return { + 'success': result.success, + 'message': result.result.get('message') if result.success else result.error, + 'details': result.result if result.success else None + } + + async def connect_and_get_tables(self, config_id: int, user_id: int) -> Dict[str, Any]: + """连接数据库并获取表列表""" + config = self.get_config_by_id(config_id, user_id) + if not config: + raise NotFoundError("数据库配置不存在") + + connection_config = { + 'host': config.host, + 'port': config.port, + 'database': config.database, + 'username': config.username, + 'password': self._decrypt_password(config.password) + } + + if 'postgresql' == config.db_type: + # 连接数据库 + connect_result = await self.postgresql_tool.execute( + operation="connect", + connection_config=connection_config, + user_id=str(user_id) + ) + elif 'mysql' == config.db_type: + # 连接数据库 + connect_result = await self.mysql_tool.execute( + operation="connect", + connection_config=connection_config, + user_id=str(user_id) + ) + + if not connect_result.success: + return { + 'success': False, + 'message': connect_result.error + } + # 连接信息已保存到PostgreSQLMCPTool的connections中 + return { + 'success': True, + 'data': connect_result.result, + 'config_name': config.name + } + + async def get_table_data(self, table_name: str, user_id: int, db_type: str, limit: int = 100) -> Dict[str, Any]: + """获取表数据预览(复用已建立的连接)""" + try: + user_id_str = str(user_id) + + # 根据db_type选择相应的数据库工具 + if db_type.lower() == 'postgresql': + db_tool = self.postgresql_tool + elif db_type.lower() == 'mysql': + db_tool = self.mysql_tool + else: + return { + 'success': False, + 'message': f'不支持的数据库类型: {db_type}' + } + + # 检查是否已有连接 + if user_id_str not in db_tool.connections: + return { + 'success': False, + 'message': '数据库连接已断开,请重新连接数据库' + } + + # 直接使用已建立的连接执行查询 + sql_query = f"SELECT * FROM {table_name}" + result = await db_tool.execute( + operation="execute_query", + user_id=user_id_str, + sql_query=sql_query, + limit=limit + ) + + if not result.success: + return { + 'success': False, + 'message': result.error + } + + return { + 'success': True, + 'data': result.result, + 'db_type': db_type + } + + except Exception as e: + logger.error(f"获取表数据失败: {str(e)}", exc_info=True) + return { + 'success': False, + 'message': f'获取表数据失败: {str(e)}' + } + + def disconnect_database(self, user_id: int) -> Dict[str, Any]: + """断开数据库连接""" + try: + # 从PostgreSQLMCPTool断开连接 + self.postgresql_tool.execute( + operation="disconnect", + user_id=str(user_id) + ) + + # 从本地连接管理中移除 + if user_id in self.user_connections: + del self.user_connections[user_id] + + return { + 'success': True, + 'message': '数据库连接已断开' + } + except Exception as e: + return { + 'success': False, + 'message': f'断开连接失败: {str(e)}' + } + + def get_config_by_type(self, user_id: int, db_type: str) -> Optional[DatabaseConfig]: + """根据数据库类型获取用户配置""" + return self.db.query(DatabaseConfig).filter( + and_( + DatabaseConfig.created_by == user_id, + DatabaseConfig.db_type == db_type, + DatabaseConfig.is_active == True + ) + ).first() + + async def create_or_update_config(self, user_id: int, config_data: Dict[str, Any]) -> DatabaseConfig: + """创建或更新数据库配置(保证db_type唯一性)""" + try: + # 检查是否已存在该类型的配置 + existing_config = self.get_config_by_type(user_id, config_data['db_type']) + + if existing_config: + # 更新现有配置 + for key, value in config_data.items(): + if key == 'password': + setattr(existing_config, key, self._encrypt_password(value)) + elif hasattr(existing_config, key): + setattr(existing_config, key, value) + + self.db.commit() + self.db.refresh(existing_config) + logger.info(f"更新数据库配置成功: {existing_config.name} (ID: {existing_config.id})") + return existing_config + else: + # 创建新配置 + return await self.create_config(user_id, config_data) + + except Exception as e: + self.db.rollback() + logger.error(f"创建或更新数据库配置失败: {str(e)}") + raise \ No newline at end of file diff --git a/backend/th_agenter/services/document.py b/backend/th_agenter/services/document.py new file mode 100644 index 0000000..fe31a7c --- /dev/null +++ b/backend/th_agenter/services/document.py @@ -0,0 +1,301 @@ +"""Document service.""" + +import os +import logging +import hashlib +import mimetypes +from pathlib import Path +from typing import List, Optional, Dict, Any +from sqlalchemy.orm import Session +from fastapi import UploadFile + +from ..models.knowledge_base import Document, KnowledgeBase +from ..core.config import get_settings +from ..utils.file_utils import FileUtils +from .storage import storage_service +from .document_processor import get_document_processor +from ..utils.schemas import DocumentChunk + +logger = logging.getLogger(__name__) +settings = get_settings() + + +class DocumentService: + """Document service for managing documents in knowledge bases.""" + + def __init__(self, db: Session): + self.db = db + self.file_utils = FileUtils() + + async def upload_document(self, file: UploadFile, kb_id: int) -> Document: + """Upload a document to knowledge base.""" + try: + # Validate knowledge base exists + kb = self.db.query(KnowledgeBase).filter(KnowledgeBase.id == kb_id).first() + if not kb: + raise ValueError(f"Knowledge base {kb_id} not found") + + # Validate file + if not file.filename: + raise ValueError("No filename provided") + + # Validate file extension + file_extension = Path(file.filename).suffix.lower() + if file_extension not in settings.file.allowed_extensions: + raise ValueError(f"File type {file_extension} not allowed") + + # Upload file using storage service + storage_info = await storage_service.upload_file(file, kb_id) + + # Create document record + document = Document( + knowledge_base_id=kb_id, + filename=os.path.basename(storage_info["file_path"]), + original_filename=file.filename, + file_path=storage_info.get("full_path", storage_info["file_path"]), # Use absolute path if available + file_size=storage_info["size"], + file_type=file_extension, + mime_type=storage_info["mime_type"], + is_processed=False + ) + + # Set audit fields + document.set_audit_fields() + + self.db.add(document) + self.db.commit() + self.db.refresh(document) + + logger.info(f"Uploaded document: {file.filename} to KB {kb_id} (Doc ID: {document.id})") + return document + + except Exception as e: + self.db.rollback() + logger.error(f"Failed to upload document: {e}") + raise + + def get_document(self, doc_id: int, kb_id: int = None) -> Optional[Document]: + """Get document by ID, optionally filtered by knowledge base.""" + query = self.db.query(Document).filter(Document.id == doc_id) + if kb_id is not None: + query = query.filter(Document.knowledge_base_id == kb_id) + return query.first() + + def get_documents(self, kb_id: int, skip: int = 0, limit: int = 50) -> List[Document]: + """Get documents in knowledge base.""" + return ( + self.db.query(Document) + .filter(Document.knowledge_base_id == kb_id) + .offset(skip) + .limit(limit) + .all() + ) + + def list_documents(self, kb_id: int, skip: int = 0, limit: int = 50) -> tuple[List[Document], int]: + """List documents in knowledge base with total count.""" + # Get total count + total = self.db.query(Document).filter(Document.knowledge_base_id == kb_id).count() + + # Get documents with pagination + documents = ( + self.db.query(Document) + .filter(Document.knowledge_base_id == kb_id) + .offset(skip) + .limit(limit) + .all() + ) + + return documents, total + + def delete_document(self, doc_id: int, kb_id: int = None) -> bool: + """Delete document.""" + try: + document = self.get_document(doc_id, kb_id) + if not document: + return False + + # Delete file from storage + try: + storage_service.delete_file(document.file_path) + logger.info(f"Deleted file: {document.file_path}") + except Exception as e: + logger.warning(f"Failed to delete file {document.file_path}: {e}") + + # TODO: Remove from vector database + # This should be implemented when vector database service is ready + get_document_processor().delete_document_from_vector_store(kb_id,doc_id) + # Delete database record + self.db.delete(document) + self.db.commit() + + logger.info(f"Deleted document: {document.filename} (ID: {doc_id})") + return True + + except Exception as e: + self.db.rollback() + logger.error(f"Failed to delete document {doc_id}: {e}") + raise + + async def process_document(self, doc_id: int, kb_id: int = None) -> Dict[str, Any]: + """Process document (extract text and create embeddings).""" + try: + document = self.get_document(doc_id, kb_id) + if not document: + raise ValueError(f"Document {doc_id} not found") + + if document.is_processed: + logger.info(f"Document {doc_id} already processed") + return { + "document_id": doc_id, + "status": "already_processed", + "message": "文档已处理" + } + + # 更新文档状态为处理中 + document.processing_error = None + self.db.commit() + + # 调用文档处理器进行处理 + result = get_document_processor().process_document( + document_id=doc_id, + file_path=document.file_path, + knowledge_base_id=document.knowledge_base_id + ) + + # 如果处理成功,更新文档状态 + if result["status"] == "success": + document.is_processed = True + document.chunk_count = result.get("chunks_count", 0) + self.db.commit() + self.db.refresh(document) + logger.info(f"Processed document: {document.filename} (ID: {doc_id})") + + return result + + except Exception as e: + self.db.rollback() + logger.error(f"Failed to process document {doc_id}: {e}") + + # Update document with error + try: + document = self.get_document(doc_id) + if document: + document.processing_error = str(e) + self.db.commit() + except Exception as db_error: + logger.error(f"Failed to update document error status: {db_error}") + + return { + "document_id": doc_id, + "status": "failed", + "error": str(e), + "message": "文档处理失败" + } + + async def _extract_text(self, document: Document) -> str: + """Extract text content from document.""" + try: + if document.is_text_file: + # Read text files directly + with open(document.file_path, 'r', encoding='utf-8') as f: + return f.read() + + elif document.is_pdf_file: + # TODO: Implement PDF text extraction using PyPDF2 or similar + # For now, return placeholder + return f"PDF content from {document.original_filename}" + + elif document.is_office_file: + # TODO: Implement Office file text extraction using python-docx, openpyxl, etc. + # For now, return placeholder + return f"Office document content from {document.original_filename}" + + else: + raise ValueError(f"Unsupported file type: {document.file_type}") + + except Exception as e: + logger.error(f"Failed to extract text from {document.file_path}: {e}") + raise + + def update_document_status(self, doc_id: int, is_processed: bool, error: Optional[str] = None) -> bool: + """Update document processing status.""" + try: + document = self.get_document(doc_id) + if not document: + return False + + document.is_processed = is_processed + document.processing_error = error + + self.db.commit() + return True + + except Exception as e: + self.db.rollback() + logger.error(f"Failed to update document status {doc_id}: {e}") + raise + + def search_documents(self, kb_id: int, query: str, limit: int = 5) -> List[Dict[str, Any]]: + """Search documents in knowledge base using vector similarity.""" + try: + # 使用文档处理器进行相似性搜索 + results = get_document_processor().search_similar_documents(kb_id, query, limit) + return results + except Exception as e: + logger.error(f"Failed to search documents in KB {kb_id}: {e}") + return [] + + def get_document_stats(self, kb_id: int) -> Dict[str, Any]: + """Get document statistics for knowledge base.""" + documents = self.get_documents(kb_id, limit=1000) # Get all documents + + total_count = len(documents) + processed_count = len([doc for doc in documents if doc.is_processed]) + total_size = sum(doc.file_size for doc in documents) + + file_types = {} + for doc in documents: + file_type = doc.file_type + file_types[file_type] = file_types.get(file_type, 0) + 1 + + return { + "total_documents": total_count, + "processed_documents": processed_count, + "pending_documents": total_count - processed_count, + "total_size_bytes": total_size, + "total_size_mb": round(total_size / (1024 * 1024), 2), + "file_types": file_types + } + + def get_document_chunks(self, doc_id: int) -> List[DocumentChunk]: + """Get document chunks for a specific document.""" + try: + # Get document to find knowledge base ID + document = self.db.query(Document).filter(Document.id == doc_id).first() + if not document: + logger.error(f"Document {doc_id} not found") + return [] + + # Get chunks from document processor + chunks_data = get_document_processor().get_document_chunks(document.knowledge_base_id, doc_id) + + # Convert to DocumentChunk objects + chunks = [] + for chunk_data in chunks_data: + chunk = DocumentChunk( + id=chunk_data["id"], + content=chunk_data["content"], + metadata=chunk_data["metadata"], + page_number=chunk_data.get("page_number"), + chunk_index=chunk_data["chunk_index"], + start_char=chunk_data.get("start_char"), + end_char=chunk_data.get("end_char") + ) + chunks.append(chunk) + + logger.info(f"Retrieved {len(chunks)} chunks for document {doc_id}") + return chunks + + except Exception as e: + logger.error(f"Failed to get chunks for document {doc_id}: {e}") + return [] \ No newline at end of file diff --git a/backend/th_agenter/services/document_processor.py b/backend/th_agenter/services/document_processor.py new file mode 100644 index 0000000..7b97c63 --- /dev/null +++ b/backend/th_agenter/services/document_processor.py @@ -0,0 +1,1005 @@ +"""文档处理服务,负责文档的分段、向量化和索引""" + +import os +import logging +from typing import List, Dict, Any, Optional +from pathlib import Path +from urllib.parse import quote +from sqlalchemy import create_engine, text +from sqlalchemy.orm import sessionmaker +from sqlalchemy.pool import QueuePool +from langchain_text_splitters import RecursiveCharacterTextSplitter + +logger = logging.getLogger(__name__) + +# Try to import pdfplumber with exception handling +try: + import pdfplumber + PDFPLUMBER_AVAILABLE = True +except ImportError: + logger.warning("pdfplumber not available. PDF processing features will be disabled.") + pdfplumber = None + PDFPLUMBER_AVAILABLE = False + +# Try to import langchain_core and langchain_postgres with exception handling +try: + from langchain_core.documents import Document + from langchain_postgres import PGVector + LANGCHAIN_CORE_AVAILABLE = True + LANGCHAIN_POSTGRES_AVAILABLE = True +except ImportError as e: + logger.warning(f"Some langchain modules not available: {e}. Document processing features may be limited.") + Document = None + PGVector = None + LANGCHAIN_CORE_AVAILABLE = False + LANGCHAIN_POSTGRES_AVAILABLE = False +from typing import List +# 旧的ZhipuEmbeddings类已移除,现在统一使用EmbeddingFactory创建embedding实例 + +from ..core.config import settings +from ..utils.file_utils import FileUtils +from ..models.knowledge_base import Document as DocumentModel +from ..db.database import get_db + +# Try to import document loaders with exception handling +try: + from langchain_community.document_loaders import ( + TextLoader, + PyPDFLoader, + Docx2txtLoader, + UnstructuredMarkdownLoader + ) + DOCUMENT_LOADERS_AVAILABLE = True +except ImportError: + logger.warning("langchain_community.document_loaders not available. Document processing features will be disabled.") + TextLoader = None + PyPDFLoader = None + Docx2txtLoader = None + UnstructuredMarkdownLoader = None + DOCUMENT_LOADERS_AVAILABLE = False + + +class PGVectorConnectionPool: + """PGVector连接池管理器""" + + def __init__(self): + self.engine = None + self.SessionLocal = None + self._init_connection_pool() + + def _init_connection_pool(self): + """初始化连接池""" + if settings.vector_db.type == "pgvector": + # 构建连接字符串,对密码进行URL编码以处理特殊字符(如@符号) + encoded_password = quote(settings.vector_db.pgvector_password, safe="") + connection_string = ( + f"postgresql://{settings.vector_db.pgvector_user}:" + f"{encoded_password}@" + f"{settings.vector_db.pgvector_host}:" + f"{settings.vector_db.pgvector_port}/" + f"{settings.vector_db.pgvector_database}" + ) + + # 创建SQLAlchemy引擎,配置连接池 + self.engine = create_engine( + connection_string, + poolclass=QueuePool, + pool_size=5, # 连接池大小 + max_overflow=10, # 最大溢出连接数 + pool_pre_ping=True, # 连接前ping检查 + pool_recycle=3600, # 连接回收时间(秒) + echo=False # 是否打印SQL语句 + ) + + # 创建会话工厂 + self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine) + logger.info(f"PGVector连接池已初始化: {settings.vector_db.pgvector_host}:{settings.vector_db.pgvector_port}") + + def get_session(self): + """获取数据库会话""" + if self.SessionLocal is None: + raise RuntimeError("连接池未初始化") + return self.SessionLocal() + + def execute_query(self, query: str, params: tuple = None): + """执行查询并返回结果""" + session = self.get_session() + try: + result = session.execute(text(query), params or {}) + return result.fetchall() + finally: + session.close() + + +class DocumentProcessor: + """文档处理器,负责文档的加载、分段和向量化""" + + def __init__(self): + # 初始化语义分割器配置 + self.semantic_splitter_enabled = settings.file.semantic_splitter_enabled + self.text_splitter = RecursiveCharacterTextSplitter( + chunk_size=settings.file.chunk_size, + chunk_overlap=settings.file.chunk_overlap, + length_function=len, + separators=["\n\n", "\n", " ", ""] + ) + + # 初始化嵌入模型 - 根据配置选择提供商 + self._init_embeddings() + + # 初始化连接池(仅对PGVector) + self.pgvector_pool = None + + # PostgreSQL pgvector连接配置 + print('settings.vector_db.type=============', settings.vector_db.type) + if settings.vector_db.type == "pgvector": + # 新版本PGVector使用psycopg3连接字符串 + # 对密码进行URL编码以处理特殊字符(如@符号) + encoded_password = quote(settings.vector_db.pgvector_password, safe="") + self.connection_string = ( + f"postgresql+psycopg://{settings.vector_db.pgvector_user}:" + f"{encoded_password}@" + f"{settings.vector_db.pgvector_host}:" + f"{settings.vector_db.pgvector_port}/" + f"{settings.vector_db.pgvector_database}" + ) + # 初始化连接池 + self.pgvector_pool = PGVectorConnectionPool() + else: + # 向量数据库存储路径(Chroma兼容) + vector_db_path = settings.vector_db.persist_directory + if not os.path.isabs(vector_db_path): + # 如果是相对路径,则基于项目根目录计算绝对路径 + # 项目根目录是backend的父目录 + backend_dir = Path(__file__).parent.parent.parent + vector_db_path = str(backend_dir / vector_db_path) + self.vector_db_path = vector_db_path + + def _init_embeddings(self): + """根据配置初始化embedding模型""" + from .embedding_factory import EmbeddingFactory + self.embeddings = EmbeddingFactory.create_embeddings() + + def load_document(self, file_path: str) -> List[Document]: + """根据文件类型加载文档""" + file_extension = Path(file_path).suffix.lower() + + try: + if file_extension == '.txt': + loader = TextLoader(file_path, encoding='utf-8') + documents = loader.load() + elif file_extension == '.pdf': + # 使用pdfplumber处理PDF文件,更稳定 + documents = self._load_pdf_with_pdfplumber(file_path) + elif file_extension == '.docx': + loader = Docx2txtLoader(file_path) + documents = loader.load() + elif file_extension == '.md': + loader = UnstructuredMarkdownLoader(file_path) + documents = loader.load() + else: + raise ValueError(f"不支持的文件类型: {file_extension}") + + logger.info(f"成功加载文档: {file_path}, 页数: {len(documents)}") + return documents + + except Exception as e: + logger.error(f"加载文档失败 {file_path}: {str(e)}") + raise + + def _load_pdf_with_pdfplumber(self, file_path: str) -> List[Document]: + """使用pdfplumber加载PDF文档""" + documents = [] + try: + with pdfplumber.open(file_path) as pdf: + for page_num, page in enumerate(pdf.pages): + text = page.extract_text() + if text and text.strip(): # 只处理有文本内容的页面 + doc = Document( + page_content=text, + metadata={ + "source": file_path, + "page": page_num + 1 + } + ) + documents.append(doc) + return documents + except Exception as e: + logger.error(f"使用pdfplumber加载PDF失败 {file_path}: {str(e)}") + # 如果pdfplumber失败,回退到PyPDFLoader + try: + loader = PyPDFLoader(file_path) + return loader.load() + except Exception as fallback_e: + logger.error(f"PyPDFLoader回退也失败 {file_path}: {str(fallback_e)}") + raise fallback_e + + def _merge_documents(self, documents: List[Document]) -> Document: + """将多个文档合并成一个文档""" + merged_text = "" + merged_metadata = {} + + for doc in documents: + if merged_text: + merged_text += "\n\n" + merged_text += doc.page_content + # 合并元数据 + merged_metadata.update(doc.metadata) + + return Document(page_content=merged_text, metadata=merged_metadata) + + def _get_semantic_split_points(self, text: str) -> List[str]: + """使用大模型分析文档内容,返回合适的分割点列表""" + try: + from langchain.chat_models import ChatOpenAI + from ..core.config import get_settings + + + + prompt = f""" + # 任务说明 + 请分析文档内容,识别出适合作为分割点的关键位置。分割点应该是能够将文档划分为有意义段落的文本片段。 + + + # 分割规则 + 请严格按照以下规则识别分割点: + + ## 基本要求 + 1. 分割点必须是完整的句子开头或段落开头 + 2. 每个分割后的部分应包含相对完整的语义内容 + 3. 每个分割部分的理想长度控制在500字以内,严禁超过1000字。如果超过了1000字,要强制分段。 + + ## 短段落处理 + 4. 如果某部分长度可能小于50字,应将其与后续内容合并,避免产生过短片段 + + ## 唯一性保证(重要) + 5. 确保每个分割点在文档中具有唯一性: + - 检查文内是否存在相同的文本片段 + - 如果存在重复,需要扩展分割点字符串,直到获得唯一标识 + - 扩展方法:在当前分割点后追加几个字符,形成更长的唯一字符串 + + ## 示例说明 + 原始文档: + "目录: + 第一章 标题一 + 第二章 标题二 + 正文 + 第一章 标题一 + 这是第一章的内容 + + 第二章 标题二 + 这是第二章的内容" + + 错误分割点:"第一章 标题一"(在目录和正文中重复出现) + + 正确分割点:"第一章 标题一\n这是第"(通过追加内容确保唯一性) + + # 输出格式 + - 只返回分割点文本字符串 + - 每个分割点用~~分隔 + - 不要包含任何其他内容或解释 + + 示例输出:分割点1~~分割点2~~分割点3 + + + 文档内容: + {text[:10000]} # 限制输入长度 + """ + from ..core.llm import create_llm + llm = create_llm(temperature=0.2) + + response = llm.invoke(prompt) + + # 解析响应获取分割点列表 + split_points = [point.strip() for point in response.content.split('~~') if point.strip()] + logger.info(f"语义分析得到 {len(split_points)} 个分割点") + return split_points + + except Exception as e: + logger.error(f"获取语义分割点失败: {str(e)}") + return [] + + def _split_by_semantic_points(self, text: str, split_points: List[str]) -> List[str]: + """根据语义分割点切分文本""" + chunks = [] + current_pos = 0 + + # 按顺序查找每个分割点并切分文本 + for point in split_points: + pos = text.find(point, current_pos) + if pos != -1: + # 添加当前位置到分割点位置的文本块 + if pos > current_pos: + chunk = text[current_pos:pos].strip() + if chunk: + chunks.append(chunk) + current_pos = pos + + # 添加最后一个文本块 + if current_pos < len(text): + chunk = text[current_pos:].strip() + if chunk: + chunks.append(chunk) + + return chunks + + def split_documents(self, documents: List[Document]) -> List[Document]: + """将文档分割成小块(含短段落合并和超长强制分割功能)""" + try: + if self.semantic_splitter_enabled and documents: + # 1. 合并文档 + merged_doc = self._merge_documents(documents) + + # 2. 获取语义分割点 + split_points = self._get_semantic_split_points(merged_doc.page_content) + + if split_points: + # 3. 根据语义分割点切分文本 + text_chunks = self._split_by_semantic_points(merged_doc.page_content, split_points) + + # 4. 处理短段落合并和超长强制分割(新增逻辑) + processed_chunks = [] + buffer = "" + for chunk in text_chunks: + # 先检查当前chunk是否超长(超过1000字符) + if len(chunk) > 1000: + # 如果有缓冲内容,先处理缓冲 + if buffer: + processed_chunks.append(buffer) + buffer = "" + + # 对超长chunk进行强制分割 + forced_splits = self._force_split_long_chunk(chunk) + processed_chunks.extend(forced_splits) + else: + # 正常处理短段落合并 + if not buffer: + buffer = chunk + else: + if len(buffer) < 100: + buffer = f"{buffer}\n{chunk}" + else: + processed_chunks.append(buffer) + buffer = chunk + + # 添加最后剩余的缓冲内容 + if buffer: + processed_chunks.append(buffer) + + # 5. 创建Document对象 + chunks = [] + for i, chunk in enumerate(processed_chunks): + doc = Document( + page_content=chunk, + metadata={ + **merged_doc.metadata, + 'chunk_index': i, + 'merged': len(chunk) > 100, # 标记是否经过合并 + 'forced_split': len(chunk) > 1000 # 标记是否经过强制分割 + } + ) + chunks.append(doc) + else: + # 如果获取分割点失败,回退到默认分割器 + logger.warning("语义分割失败,使用默认分割器") + chunks = self.text_splitter.split_documents(documents) + else: + # 使用默认分割器 + chunks = self.text_splitter.split_documents(documents) + + logger.info(f"文档分割完成,共生成 {len(chunks)} 个文档块") + return chunks + + except Exception as e: + logger.error(f"文档分割失败: {str(e)}") + raise + + def _force_split_long_chunk(self, chunk: str) -> List[str]: + """强制分割超长段落(超过1000字符)""" + max_length = 1000 + chunks = [] + + # 先尝试按换行符分割 + if '\n' in chunk: + lines = chunk.split('\n') + current_chunk = "" + for line in lines: + if len(current_chunk) + len(line) + 1 > max_length: + if current_chunk: + chunks.append(current_chunk) + current_chunk = line + else: + chunks.append(line[:max_length]) + current_chunk = line[max_length:] + else: + if current_chunk: + current_chunk += "\n" + line + else: + current_chunk = line + if current_chunk: + chunks.append(current_chunk) + else: + # 没有换行符则直接按长度分割 + chunks = [chunk[i:i + max_length] for i in range(0, len(chunk), max_length)] + + return chunks + + def create_vector_store(self, knowledge_base_id: int, documents: List[Document], document_id: int = None) -> str: + """为知识库创建向量存储""" + try: + if settings.vector_db.type == "pgvector": + # 添加元数据 + for i, doc in enumerate(documents): + doc.metadata.update({ + "knowledge_base_id": knowledge_base_id, + "document_id": str(document_id) if document_id else "unknown", + "chunk_id": f"{knowledge_base_id}_{document_id}_{i}", + "chunk_index": i + }) + + # 创建PostgreSQL pgvector存储 + collection_name = f"{settings.vector_db.pgvector_table_name}_kb_{knowledge_base_id}" + + # 创建新版本PGVector实例 + vector_store = PGVector( + connection=self.connection_string, + embeddings=self.embeddings, + collection_name=collection_name, + use_jsonb=True # 使用JSONB存储元数据 + ) + + # 手动添加文档 + vector_store.add_documents(documents) + + logger.info(f"PostgreSQL pgvector存储创建成功: {collection_name}") + return collection_name + else: + # Chroma兼容模式 + from langchain_community.vectorstores import Chroma + kb_vector_path = os.path.join(self.vector_db_path, f"kb_{knowledge_base_id}") + + # 添加元数据 + for i, doc in enumerate(documents): + doc.metadata.update({ + "knowledge_base_id": knowledge_base_id, + "document_id": str(document_id) if document_id else "unknown", + "chunk_id": f"{knowledge_base_id}_{document_id}_{i}", + "chunk_index": i + }) + + # 创建向量存储 + vector_store = Chroma.from_documents( + documents=documents, + embedding=self.embeddings, + persist_directory=kb_vector_path + ) + + # 持久化向量存储 + vector_store.persist() + + logger.info(f"向量存储创建成功: {kb_vector_path}") + return kb_vector_path + + except Exception as e: + logger.error(f"创建向量存储失败: {str(e)}") + raise + + def add_documents_to_vector_store(self, knowledge_base_id: int, documents: List[Document], document_id: int = None) -> None: + """向现有向量存储添加文档""" + try: + if settings.vector_db.type == "pgvector": + # 添加元数据 + for i, doc in enumerate(documents): + doc.metadata.update({ + "knowledge_base_id": knowledge_base_id, + "document_id": str(document_id) if document_id else "unknown", + "chunk_id": f"{knowledge_base_id}_{document_id}_{i}", + "chunk_index": i + }) + + # PostgreSQL pgvector存储 + collection_name = f"{settings.vector_db.pgvector_table_name}_kb_{knowledge_base_id}" + try: + # 连接到现有集合 + vector_store = PGVector( + connection=self.connection_string, + embeddings=self.embeddings, + collection_name=collection_name, + use_jsonb=True + ) + # 添加新文档 + vector_store.add_documents(documents) + except Exception as e: + # 如果集合不存在,创建新的向量存储 + logger.warning(f"连接现有向量存储失败,创建新的向量存储: {e}") + self.create_vector_store(knowledge_base_id, documents, document_id) + return + + logger.info(f"文档已添加到PostgreSQL pgvector存储: {collection_name}") + else: + # Chroma兼容模式 + from langchain_community.vectorstores import Chroma + kb_vector_path = os.path.join(self.vector_db_path, f"kb_{knowledge_base_id}") + + # 检查向量存储是否存在 + if not os.path.exists(kb_vector_path): + # 如果不存在,创建新的向量存储 + self.create_vector_store(knowledge_base_id, documents, document_id) + return + + # 添加元数据 + for i, doc in enumerate(documents): + doc.metadata.update({ + "knowledge_base_id": knowledge_base_id, + "document_id": str(document_id) if document_id else "unknown", + "chunk_id": f"{knowledge_base_id}_{document_id}_{i}", + "chunk_index": i + }) + + # 加载现有向量存储 + vector_store = Chroma( + persist_directory=kb_vector_path, + embedding_function=self.embeddings + ) + + # 添加新文档 + vector_store.add_documents(documents) + vector_store.persist() + + logger.info(f"文档已添加到向量存储: {kb_vector_path}") + + except Exception as e: + logger.error(f"添加文档到向量存储失败: {str(e)}") + raise + + def process_document(self, document_id: int, file_path: str, knowledge_base_id: int) -> Dict[str, Any]: + """处理单个文档:加载、分段、向量化""" + try: + logger.info(f"开始处理文档 ID: {document_id}, 路径: {file_path}") + + # 1. 加载文档 + documents = self.load_document(file_path) + + # 2. 分割文档 + chunks = self.split_documents(documents) + + # 3. 添加到向量存储 + self.add_documents_to_vector_store(knowledge_base_id, chunks, document_id) + + # 4. 更新文档状态 + with next(get_db()) as db: + document = db.query(DocumentModel).filter(DocumentModel.id == document_id).first() + if document: + document.status = "processed" + document.chunk_count = len(chunks) + db.commit() + + result = { + "document_id": document_id, + "status": "success", + "chunks_count": len(chunks), + "message": "文档处理完成" + } + + logger.info(f"文档处理完成: {result}") + return result + + except Exception as e: + logger.error(f"文档处理失败 ID: {document_id}: {str(e)}") + + # 更新文档状态为失败 + try: + with next(get_db()) as db: + document = db.query(DocumentModel).filter(DocumentModel.id == document_id).first() + if document: + document.status = "failed" + document.error_message = str(e) + db.commit() + except Exception as db_error: + logger.error(f"更新文档状态失败: {str(db_error)}") + + return { + "document_id": document_id, + "status": "failed", + "error": str(e), + "message": "文档处理失败" + } + + def _get_document_ids_from_vector_store(self, knowledge_base_id: int, document_id: int) -> List[str]: + """查询指定document_id的所有向量记录的uuid""" + try: + collection_name = f"{settings.vector_db.pgvector_table_name}_kb_{knowledge_base_id}" + + # 使用连接池执行查询 + if self.pgvector_pool: + query = f""" + SELECT uuid FROM langchain_pg_embedding + WHERE collection_id = ( + SELECT uuid FROM langchain_pg_collection + WHERE name = %s + ) AND cmetadata->>'document_id' = %s + """ + + result = self.pgvector_pool.execute_query(query, (collection_name, str(document_id))) + return [row[0] for row in result] if result else [] + else: + logger.warning("PGVector连接池未初始化") + return [] + + except Exception as e: + logger.error(f"查询文档向量记录失败: {str(e)}") + return [] + + def delete_document_from_vector_store(self, knowledge_base_id: int, document_id: int) -> None: + """从向量存储中删除文档""" + try: + if settings.vector_db.type == "pgvector": + # PostgreSQL pgvector存储 + collection_name = f"{settings.vector_db.pgvector_table_name}_kb_{knowledge_base_id}" + + try: + # 创建新版本PGVector实例 + vector_store = PGVector( + connection=self.connection_string, + embeddings=self.embeddings, + collection_name=collection_name, + use_jsonb=True + ) + + # 直接从数据库查询要删除的文档UUID + try: + from sqlalchemy import text + from sqlalchemy.orm import Session + + # 获取数据库引擎 + engine = vector_store._engine + + with Session(engine) as session: + # 查询匹配document_id的所有记录的ID + query_sql = text( + f"SELECT id FROM langchain_pg_embedding " + f"WHERE cmetadata->>'document_id' = :doc_id" + ) + result = session.execute(query_sql, {"doc_id": str(document_id)}) + ids_to_delete = [row[0] for row in result.fetchall()] + + if ids_to_delete: + # 使用ID删除文档 + vector_store.delete(ids=ids_to_delete) + logger.info(f"成功删除 {len(ids_to_delete)} 个文档块: document_id={document_id}") + else: + logger.warning(f"未找到要删除的文档ID: document_id={document_id}") + + except Exception as query_error: + logger.error(f"查询要删除的文档时出错: {query_error}") + # 如果查询失败,说明文档可能不存在 + logger.warning(f"无法查询到要删除的文档: document_id={document_id}") + return + + logger.info(f"文档已从PostgreSQL pgvector存储中删除: document_id={document_id}") + except Exception as e: + logger.warning(f"PostgreSQL pgvector存储不存在或删除失败: {collection_name}, {str(e)}") + else: + # Chroma兼容模式 + from langchain_community.vectorstores import Chroma + kb_vector_path = os.path.join(self.vector_db_path, f"kb_{knowledge_base_id}") + + if not os.path.exists(kb_vector_path): + logger.warning(f"向量存储不存在: {kb_vector_path}") + return + + # 加载向量存储 + vector_store = Chroma( + persist_directory=kb_vector_path, + embedding_function=self.embeddings + ) + + # 删除相关文档块(这里需要根据实际的Chroma API来实现) + # 注意:Chroma的删除功能可能需要特定的实现方式 + logger.info(f"文档已从向量存储中删除: document_id={document_id}") + + except Exception as e: + logger.error(f"从向量存储删除文档失败: {str(e)}") + raise + + def get_document_chunks(self, knowledge_base_id: int, document_id: int) -> List[Dict[str, Any]]: + """获取文档的所有分段内容 + + 改进说明: + - 避免使用空查询进行相似性搜索,防止触发不必要的embedding API调用 + - 优先使用直接SQL查询,提高性能 + - 确保结果按chunk_index排序 + """ + try: + if settings.vector_db.type == "pgvector": + # PostgreSQL pgvector存储 - 使用直接SQL查询避免相似性搜索 + collection_name = f"{settings.vector_db.pgvector_table_name}_kb_{knowledge_base_id}" + + try: + # 尝试直接SQL查询(推荐方法) + chunks = self._get_chunks_by_sql(knowledge_base_id, document_id) + if chunks: + return chunks + + # 如果SQL查询失败,回退到改进的LangChain方法 + logger.info("SQL查询失败,使用LangChain回退方案") + return self._get_chunks_by_langchain_improved(knowledge_base_id, document_id, collection_name) + + except Exception as e: + logger.warning(f"PostgreSQL pgvector存储访问失败: {collection_name}, {str(e)}") + return [] + else: + # Chroma兼容模式 + return self._get_chunks_chroma(knowledge_base_id, document_id) + + except Exception as e: + logger.error(f"获取文档分段失败 document_id: {document_id}, kb_id: {knowledge_base_id}: {str(e)}") + return [] + + def _get_chunks_by_sql(self, knowledge_base_id: int, document_id: int) -> List[Dict[str, Any]]: + """使用SQLAlchemy连接池查询获取文档分段(推荐方法)""" + try: + if not self.pgvector_pool: + logger.error("PGVector连接池未初始化") + return [] + + # 直接SQL查询,避免相似性搜索和embedding计算 + query = """ + SELECT + id, + document, + cmetadata + FROM langchain_pg_embedding + WHERE cmetadata->>'document_id' = :document_id + AND cmetadata->>'knowledge_base_id' = :knowledge_base_id + ORDER BY + CAST(cmetadata->>'chunk_index' AS INTEGER) ASC; + """ + + # 使用连接池执行查询 + session = self.pgvector_pool.get_session() + try: + result = session.execute( + text(query), + { + 'document_id': str(document_id), + 'knowledge_base_id': str(knowledge_base_id) + } + ) + results = result.fetchall() + + chunks = [] + for row in results: + # SQLAlchemy结果行访问 + metadata = row.cmetadata + chunk = { + "id": f"chunk_{document_id}_{metadata.get('chunk_index', 0)}", + "content": row.document, + "metadata": metadata, + "page_number": metadata.get("page"), + "chunk_index": metadata.get("chunk_index", 0), + "start_char": metadata.get("start_char"), + "end_char": metadata.get("end_char") + } + chunks.append(chunk) + + logger.info(f"通过SQLAlchemy连接池查询获取到文档 {document_id} 的 {len(chunks)} 个分段") + return chunks + + finally: + session.close() + + except Exception as e: + logger.error(f"SQLAlchemy连接池查询失败: {e}") + return [] + + def _get_chunks_by_langchain_improved(self, knowledge_base_id: int, document_id: int, collection_name: str) -> List[Dict[str, Any]]: + """改进的LangChain查询方法(回退方案)""" + try: + vector_store = PGVector( + connection=self.connection_string, + embeddings=self.embeddings, + collection_name=collection_name, + use_jsonb=True + ) + + # 使用有意义的查询而不是空查询,避免触发embedding API错误 + # 先尝试获取少量结果来构造查询 + try: + sample_results = vector_store.similarity_search( + query="文档内容", # 使用通用查询词而非空字符串 + k=5, + filter={"document_id": {"$eq": str(document_id)}} + ) + + if sample_results: + # 使用第一个结果的内容片段作为查询 + first_content = sample_results[0].page_content[:50] + results = vector_store.similarity_search( + query=first_content, + k=1000, + filter={"document_id": {"$eq": str(document_id)}} + ) + else: + # 如果没有结果,尝试不使用filter的查询 + results = vector_store.similarity_search( + query="文档", + k=1000 + ) + # 手动过滤结果 + results = [doc for doc in results if doc.metadata.get("document_id") == str(document_id)] + + except Exception as e: + logger.warning(f"改进的相似性搜索失败: {e}") + return [] + + chunks = [] + for i, doc in enumerate(results): + chunk = { + "id": f"chunk_{document_id}_{i}", + "content": doc.page_content, + "metadata": doc.metadata, + "page_number": doc.metadata.get("page"), + "chunk_index": doc.metadata.get("chunk_index", i), + "start_char": doc.metadata.get("start_char"), + "end_char": doc.metadata.get("end_char") + } + chunks.append(chunk) + + # 按chunk_index排序 + chunks.sort(key=lambda x: x.get("chunk_index", 0)) + + logger.info(f"通过改进的LangChain方法获取到文档 {document_id} 的 {len(chunks)} 个分段") + return chunks + + except Exception as e: + logger.error(f"LangChain改进方法失败: {e}") + return [] + + def _get_chunks_chroma(self, knowledge_base_id: int, document_id: int) -> List[Dict[str, Any]]: + """Chroma存储的处理逻辑""" + try: + from langchain_community.vectorstores import Chroma + + # 构建向量数据库路径 + vector_db_path = os.path.join(self.vector_db_path, f"kb_{knowledge_base_id}") + + if not os.path.exists(vector_db_path): + logger.warning(f"向量数据库不存在: {vector_db_path}") + return [] + + # 加载向量数据库 + vectorstore = Chroma( + persist_directory=vector_db_path, + embedding_function=self.embeddings + ) + + # 获取所有文档的元数据,筛选出指定文档的分段 + collection = vectorstore._collection + all_docs = collection.get(include=["metadatas", "documents"]) + + chunks = [] + chunk_index = 0 + + for i, metadata in enumerate(all_docs["metadatas"]): + if metadata.get("document_id") == str(document_id): + chunk_content = all_docs["documents"][i] + + chunk = { + "id": f"chunk_{document_id}_{chunk_index}", + "content": chunk_content, + "metadata": metadata, + "page_number": metadata.get("page"), + "chunk_index": chunk_index, + "start_char": metadata.get("start_char"), + "end_char": metadata.get("end_char") + } + chunks.append(chunk) + chunk_index += 1 + + logger.info(f"获取到文档 {document_id} 的 {len(chunks)} 个分段") + return chunks + + except Exception as e: + logger.error(f"Chroma存储处理失败: {e}") + return [] + + def search_similar_documents(self, knowledge_base_id: int, query: str, k: int = 5) -> List[Dict[str, Any]]: + """在知识库中搜索相似文档""" + try: + if settings.vector_db.type == "pgvector": + # PostgreSQL pgvector存储 + collection_name = f"{settings.vector_db.pgvector_table_name}_kb_{knowledge_base_id}" + + try: + vector_store = PGVector( + connection=self.connection_string, + embeddings=self.embeddings, + collection_name=collection_name, + use_jsonb=True + ) + + # 执行相似性搜索 + results = vector_store.similarity_search_with_score(query, k=k) + + # 格式化结果 + formatted_results = [] + for doc, distance_score in results: + # pgvector使用余弦距离,距离越小相似度越高 + # 将距离转换为0-1之间的相似度分数 + similarity_score = 1.0 / (1.0 + distance_score) + + formatted_results.append({ + "content": doc.page_content, + "metadata": doc.metadata, + "similarity_score": distance_score, # 保留原始距离分数 + "normalized_score": similarity_score, # 归一化相似度分数 + "source": doc.metadata.get('filename', 'unknown'), + "document_id": doc.metadata.get('document_id', 'unknown'), + "chunk_id": doc.metadata.get('chunk_id', 'unknown') + }) + + # 按相似度分数排序(距离越小越相似) + formatted_results.sort(key=lambda x: x['similarity_score']) + + logger.info(f"PostgreSQL pgvector搜索完成,找到 {len(formatted_results)} 个相关文档") + return formatted_results + + except Exception as e: + logger.warning(f"PostgreSQL pgvector存储不存在: {collection_name}, {str(e)}") + return [] + else: + # Chroma兼容模式 + from langchain_community.vectorstores import Chroma + kb_vector_path = os.path.join(self.vector_db_path, f"kb_{knowledge_base_id}") + + if not os.path.exists(kb_vector_path): + logger.warning(f"向量存储不存在: {kb_vector_path}") + return [] + + # 加载向量存储 + vector_store = Chroma( + persist_directory=kb_vector_path, + embedding_function=self.embeddings + ) + + # 执行相似性搜索 + results = vector_store.similarity_search_with_score(query, k=k) + + # 格式化结果 + formatted_results = [] + for doc, distance_score in results: + # Chroma使用欧几里得距离,距离越小相似度越高 + # 将距离转换为0-1之间的相似度分数 + similarity_score = 1.0 / (1.0 + distance_score) + + formatted_results.append({ + "content": doc.page_content, + "metadata": doc.metadata, + "similarity_score": distance_score, # 保留原始距离分数 + "normalized_score": similarity_score, # 归一化相似度分数 + "source": doc.metadata.get('filename', 'unknown'), + "document_id": doc.metadata.get('document_id', 'unknown'), + "chunk_id": doc.metadata.get('chunk_id', 'unknown') + }) + + # 按相似度分数排序(距离越小越相似) + formatted_results.sort(key=lambda x: x['similarity_score']) + + logger.info(f"搜索完成,找到 {len(formatted_results)} 个相关文档") + return formatted_results + + except Exception as e: + logger.error(f"搜索文档失败: {str(e)}") + return [] # 返回空列表而不是抛出异常 + + +# 全局文档处理器实例(延迟初始化) +document_processor = None + +def get_document_processor(): + """获取文档处理器实例(延迟初始化)""" + global document_processor + if document_processor is None: + document_processor = DocumentProcessor() + return document_processor \ No newline at end of file diff --git a/backend/th_agenter/services/embedding_factory.py b/backend/th_agenter/services/embedding_factory.py new file mode 100644 index 0000000..b55ed23 --- /dev/null +++ b/backend/th_agenter/services/embedding_factory.py @@ -0,0 +1,96 @@ +"""Embedding factory for different providers.""" + +from typing import Optional +from langchain_core.embeddings import Embeddings +from langchain_openai import OpenAIEmbeddings +# Try to import HuggingFace embeddings with exception handling +try: + from langchain_community.embeddings import HuggingFaceEmbeddings +except ImportError: + HuggingFaceEmbeddings = None +from .zhipu_embeddings import ZhipuOpenAIEmbeddings +from ..core.config import settings +from ..utils.logger import get_logger + +logger = get_logger("embedding_factory") + + +class EmbeddingFactory: + """Factory class for creating embedding instances based on provider.""" + + @staticmethod + def create_embeddings( + provider: Optional[str] = None, + model: Optional[str] = None, + dimensions: Optional[int] = None + ) -> Embeddings: + """Create embeddings instance based on provider. + + Args: + provider: Embedding provider (openai, zhipu, deepseek, doubao, moonshot, sentence-transformers) + model: Model name + dimensions: Embedding dimensions + + Returns: + Embeddings instance + """ + # 使用新的embedding配置 + embedding_config = settings.embedding.get_current_config() + provider = provider or settings.embedding.provider + model = model or embedding_config.get("model") + dimensions = dimensions or settings.vector_db.embedding_dimension + + logger.info(f"Creating embeddings with provider: {provider}, model: {model}") + + if provider == "openai": + return EmbeddingFactory._create_openai_embeddings(embedding_config, model, dimensions) + elif provider in ["zhipu", "deepseek", "doubao", "moonshot"]: + return EmbeddingFactory._create_openai_compatible_embeddings(embedding_config, model, dimensions, provider) + elif provider == "sentence-transformers": + if not HuggingFaceEmbeddings: + raise ValueError("HuggingFace embeddings are not available. Please install langchain-community.") + return EmbeddingFactory._create_huggingface_embeddings(model) + else: + raise ValueError(f"Unsupported embedding provider: {provider}") + + @staticmethod + def _create_openai_embeddings(embedding_config: dict, model: str, dimensions: int) -> OpenAIEmbeddings: + """Create OpenAI embeddings.""" + return OpenAIEmbeddings( + api_key=embedding_config["api_key"], + base_url=embedding_config["base_url"], + model=model if model.startswith("text-embedding") else "text-embedding-ada-002", + dimensions=dimensions if model.startswith("text-embedding-3") else None + ) + + + + @staticmethod + def _create_openai_compatible_embeddings(embedding_config: dict, model: str, dimensions: int, provider: str) -> Embeddings: + """Create OpenAI-compatible embeddings for ZhipuAI, DeepSeek, Doubao, Moonshot.""" + if provider == "zhipu": + return ZhipuOpenAIEmbeddings( + api_key=embedding_config["api_key"], + base_url=embedding_config["base_url"], + model=model if model.startswith("embedding") else "embedding-3", + dimensions=dimensions + ) + else: + return OpenAIEmbeddings( + api_key=embedding_config["api_key"], + base_url=embedding_config["base_url"], + model=model, + dimensions=dimensions + ) + + @staticmethod + def _create_huggingface_embeddings(model: str) -> HuggingFaceEmbeddings: + """Create HuggingFace embeddings.""" + if not HuggingFaceEmbeddings: + raise ValueError("HuggingFaceEmbeddings is not available. Please install langchain-community.") + + return HuggingFaceEmbeddings( + model_name=model, + model_kwargs={'device': 'cpu'}, + encode_kwargs={'normalize_embeddings': True} + ) \ No newline at end of file diff --git a/backend/th_agenter/services/excel_metadata_service.py b/backend/th_agenter/services/excel_metadata_service.py new file mode 100644 index 0000000..3989fc0 --- /dev/null +++ b/backend/th_agenter/services/excel_metadata_service.py @@ -0,0 +1,241 @@ +"""Excel metadata extraction service.""" + +import os +import pandas as pd +from typing import Dict, List, Any, Optional, Tuple +from sqlalchemy.orm import Session +from ..models.excel_file import ExcelFile +from ..db.database import get_db +import logging + +logger = logging.getLogger(__name__) + + +class ExcelMetadataService: + """Service for extracting and managing Excel file metadata.""" + + def __init__(self, db: Session): + self.db = db + + def extract_file_metadata(self, file_path: str, original_filename: str, + user_id: int, file_size: int) -> Dict[str, Any]: + """Extract metadata from Excel file.""" + try: + # Determine file type + file_extension = os.path.splitext(original_filename)[1].lower() + + # Read Excel file + if file_extension == '.csv': + # For CSV files, treat as single sheet + df = pd.read_csv(file_path) + sheets_data = {'Sheet1': df} + else: + # For Excel files, read all sheets + sheets_data = pd.read_excel(file_path, sheet_name=None) + + # Extract metadata for each sheet + sheet_names = list(sheets_data.keys()) + columns_info = {} + preview_data = {} + data_types = {} + total_rows = {} + total_columns = {} + + for sheet_name, df in sheets_data.items(): + # Clean column names (remove unnamed columns) + df = df.loc[:, ~df.columns.str.contains('^Unnamed')] + + # Get column information - ensure proper encoding + columns_info[sheet_name] = [str(col) if not isinstance(col, str) else col for col in df.columns.tolist()] + + # Get preview data (first 5 rows) and convert to JSON serializable format + preview_df = df.head(5) + # Convert all values to strings to ensure JSON serialization + preview_values = [] + for row in preview_df.values: + string_row = [] + for value in row: + if pd.isna(value): + string_row.append(None) + elif hasattr(value, 'strftime'): # Handle datetime/timestamp objects + string_row.append(value.strftime('%Y-%m-%d %H:%M:%S')) + else: + # Preserve Chinese characters and other unicode content + if isinstance(value, str): + string_row.append(value) + else: + string_row.append(str(value)) + preview_values.append(string_row) + preview_data[sheet_name] = preview_values + + # Get data types + data_types[sheet_name] = {col: str(dtype) for col, dtype in df.dtypes.items()} + + # Get statistics + total_rows[sheet_name] = len(df) + total_columns[sheet_name] = len(df.columns) + + # Determine default sheet + default_sheet = sheet_names[0] if sheet_names else None + + return { + 'sheet_names': sheet_names, + 'default_sheet': default_sheet, + 'columns_info': columns_info, + 'preview_data': preview_data, + 'data_types': data_types, + 'total_rows': total_rows, + 'total_columns': total_columns, + 'is_processed': True, + 'processing_error': None + } + + except Exception as e: + logger.error(f"Error extracting metadata from {file_path}: {str(e)}") + return { + 'sheet_names': [], + 'default_sheet': None, + 'columns_info': {}, + 'preview_data': {}, + 'data_types': {}, + 'total_rows': {}, + 'total_columns': {}, + 'is_processed': False, + 'processing_error': str(e) + } + + def save_file_metadata(self, file_path: str, original_filename: str, + user_id: int, file_size: int) -> ExcelFile: + """Extract and save Excel file metadata to database.""" + try: + # Extract metadata + metadata = self.extract_file_metadata(file_path, original_filename, user_id, file_size) + + # Determine file type + file_extension = os.path.splitext(original_filename)[1].lower() + + # Create ExcelFile record + excel_file = ExcelFile( + original_filename=original_filename, + file_path=file_path, + file_size=file_size, + file_type=file_extension, + sheet_names=metadata['sheet_names'], + default_sheet=metadata['default_sheet'], + columns_info=metadata['columns_info'], + preview_data=metadata['preview_data'], + data_types=metadata['data_types'], + total_rows=metadata['total_rows'], + total_columns=metadata['total_columns'], + is_processed=metadata['is_processed'], + processing_error=metadata['processing_error'] + ) + + + # Save to database + self.db.add(excel_file) + self.db.commit() + self.db.refresh(excel_file) + + logger.info(f"Saved metadata for file {original_filename} with ID {excel_file.id}") + return excel_file + + except Exception as e: + logger.error(f"Error saving metadata for {original_filename}: {str(e)}") + self.db.rollback() + raise + + def get_user_files(self, user_id: int, skip: int = 0, limit: int = 50) -> Tuple[List[ExcelFile], int]: + """Get Excel files for a user with pagination.""" + try: + # Get total count + total = self.db.query(ExcelFile).filter(ExcelFile.created_by == user_id).count() + + # Get files with pagination + files = (self.db.query(ExcelFile) + .filter(ExcelFile.created_by == user_id) + .order_by(ExcelFile.created_at.desc()) + .offset(skip) + .limit(limit) + .all()) + + return files, total + + except Exception as e: + logger.error(f"Error getting user files for user {user_id}: {str(e)}") + return [], 0 + + def get_file_by_id(self, file_id: int, user_id: int) -> Optional[ExcelFile]: + """Get Excel file by ID and user ID.""" + try: + return (self.db.query(ExcelFile) + .filter(ExcelFile.id == file_id, ExcelFile.created_by == user_id) + .first()) + except Exception as e: + logger.error(f"Error getting file {file_id} for user {user_id}: {str(e)}") + return None + + def delete_file(self, file_id: int, user_id: int) -> bool: + """Delete Excel file record and physical file.""" + try: + # Get file record + excel_file = self.get_file_by_id(file_id, user_id) + if not excel_file: + return False + + # Delete physical file if exists + if os.path.exists(excel_file.file_path): + os.remove(excel_file.file_path) + logger.info(f"Deleted physical file: {excel_file.file_path}") + + # Delete database record + self.db.delete(excel_file) + self.db.commit() + + logger.info(f"Deleted Excel file record with ID {file_id}") + return True + + except Exception as e: + logger.error(f"Error deleting file {file_id}: {str(e)}") + self.db.rollback() + return False + + def update_last_accessed(self, file_id: int, user_id: int) -> bool: + """Update last accessed time for a file.""" + try: + excel_file = self.get_file_by_id(file_id, user_id) + if not excel_file: + return False + + from sqlalchemy.sql import func + excel_file.last_accessed = func.now() + self.db.commit() + + return True + + except Exception as e: + logger.error(f"Error updating last accessed for file {file_id}: {str(e)}") + self.db.rollback() + return False + + def get_file_summary_for_llm(self, user_id: int) -> List[Dict[str, Any]]: + """Get file summary information for LLM context.""" + try: + files = self.db.query(ExcelFile).filter(ExcelFile.user_id == user_id).all() + + summary = [] + for file in files: + file_info = { + 'file_id': file.id, + 'filename': file.original_filename, + 'file_type': file.file_type, + 'sheets': file.get_all_sheets_summary(), + 'upload_time': file.upload_time.isoformat() if file.upload_time else None + } + summary.append(file_info) + + return summary + + except Exception as e: + logger.error(f"Error getting file summary for user {user_id}: {str(e)}") + return [] \ No newline at end of file diff --git a/backend/th_agenter/services/knowledge_base.py b/backend/th_agenter/services/knowledge_base.py new file mode 100644 index 0000000..774db7f --- /dev/null +++ b/backend/th_agenter/services/knowledge_base.py @@ -0,0 +1,167 @@ +"""Knowledge base service.""" + +import logging +from typing import List, Optional, Dict, Any +from sqlalchemy.orm import Session +from sqlalchemy import and_, or_ + +from ..models.knowledge_base import KnowledgeBase +from ..utils.schemas import KnowledgeBaseCreate, KnowledgeBaseUpdate +from ..core.config import get_settings +from .document_processor import get_document_processor +from ..core.context import UserContext +logger = logging.getLogger(__name__) +settings = get_settings() + + +class KnowledgeBaseService: + """Knowledge base service for managing knowledge bases.""" + + def __init__(self, db: Session): + self.db = db + + def create_knowledge_base(self, kb_data: KnowledgeBaseCreate) -> KnowledgeBase: + """Create a new knowledge base.""" + try: + # Generate collection name for vector database + collection_name = f"kb_{kb_data.name.lower().replace(' ', '_').replace('-', '_')}" + + kb = KnowledgeBase( + name=kb_data.name, + description=kb_data.description, + embedding_model=kb_data.embedding_model, + chunk_size=kb_data.chunk_size, + chunk_overlap=kb_data.chunk_overlap, + vector_db_type=settings.vector_db.type, + collection_name=collection_name + ) + + # Set audit fields + kb.set_audit_fields() + + self.db.add(kb) + self.db.commit() + self.db.refresh(kb) + + logger.info(f"Created knowledge base: {kb.name} (ID: {kb.id})") + return kb + + except Exception as e: + self.db.rollback() + logger.error(f"Failed to create knowledge base: {e}") + raise + + def get_knowledge_base(self, kb_id: int) -> Optional[KnowledgeBase]: + """Get knowledge base by ID.""" + return self.db.query(KnowledgeBase).filter(KnowledgeBase.id == kb_id).first() + + def get_knowledge_base_by_name(self, name: str) -> Optional[KnowledgeBase]: + """Get knowledge base by name.""" + return self.db.query(KnowledgeBase).filter(and_( + KnowledgeBase.name == name, + KnowledgeBase.created_by == UserContext.get_current_user().id + )).first() + + def get_knowledge_bases(self, skip: int = 0, limit: int = 50, active_only: bool = True) -> List[KnowledgeBase]: + """Get list of knowledge bases.""" + + return self.db.query(KnowledgeBase).filter(KnowledgeBase.created_by == UserContext.get_current_user().id) \ + .offset(skip).limit(limit).all() + + + def update_knowledge_base(self, kb_id: int, kb_update: KnowledgeBaseUpdate) -> Optional[KnowledgeBase]: + """Update knowledge base.""" + try: + kb = self.get_knowledge_base(kb_id) + if not kb: + return None + + # Update fields + update_data = kb_update.model_dump(exclude_unset=True) + for field, value in update_data.items(): + setattr(kb, field, value) + + # Set audit fields + kb.set_audit_fields(is_update=True) + + self.db.commit() + self.db.refresh(kb) + + logger.info(f"Updated knowledge base: {kb.name} (ID: {kb.id})") + return kb + + except Exception as e: + self.db.rollback() + logger.error(f"Failed to update knowledge base {kb_id}: {e}") + raise + + def delete_knowledge_base(self, kb_id: int) -> bool: + """Delete knowledge base.""" + try: + kb = self.get_knowledge_base(kb_id) + if not kb: + return False + + # TODO: Clean up vector database collection + # This should be implemented when vector database service is ready + + self.db.delete(kb) + self.db.commit() + + logger.info(f"Deleted knowledge base: {kb.name} (ID: {kb.id})") + return True + + except Exception as e: + self.db.rollback() + logger.error(f"Failed to delete knowledge base {kb_id}: {e}") + raise + + def search_knowledge_bases(self, query: str, skip: int = 0, limit: int = 50) -> List[KnowledgeBase]: + """Search knowledge bases by name or description.""" + search_filter = or_( + KnowledgeBase.name.ilike(f"%{query}%"), + KnowledgeBase.description.ilike(f"%{query}%") + ) + + return ( + self.db.query(KnowledgeBase) + .filter(and_(KnowledgeBase.is_active == True, search_filter)) + .offset(skip) + .limit(limit) + .all() + ) + + async def search(self, kb_id: int, query: str, top_k: int = 5, similarity_threshold: float = 0.7) -> List[Dict[str, Any]]: + """Search in knowledge base using vector similarity.""" + try: + logger.info(f"Searching in knowledge base {kb_id} for: {query}") + + # 使用document_processor进行向量搜索 + search_results = get_document_processor().search_similar_documents( + knowledge_base_id=kb_id, + query=query, + k=top_k + ) + + # 过滤相似度阈值 + filtered_results = [] + for result in search_results: + # 使用已经归一化的相似度分数 + normalized_score = result.get('normalized_score', 0) + + if normalized_score >= similarity_threshold: + filtered_results.append({ + "content": result.get('content', ''), + "source": result.get('source', 'unknown'), + "score": normalized_score, + "metadata": result.get('metadata', {}), + "document_id": result.get('document_id', 'unknown'), + "chunk_id": result.get('chunk_id', 'unknown') + }) + + logger.info(f"Found {len(filtered_results)} relevant documents (threshold: {similarity_threshold})") + return filtered_results + + except Exception as e: + logger.error(f"Search failed for knowledge base {kb_id}: {str(e)}") + return [] \ No newline at end of file diff --git a/backend/th_agenter/services/knowledge_chat.py b/backend/th_agenter/services/knowledge_chat.py new file mode 100644 index 0000000..3c56acb --- /dev/null +++ b/backend/th_agenter/services/knowledge_chat.py @@ -0,0 +1,387 @@ +"""Knowledge base chat service using LangChain RAG.""" + +import asyncio +from typing import List, Dict, Any, Optional, AsyncGenerator +from sqlalchemy.orm import Session + +from langchain_openai import ChatOpenAI +from langchain_core.messages import HumanMessage, AIMessage, SystemMessage +from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder +from langchain_core.runnables import RunnablePassthrough +from langchain_core.output_parsers import StrOutputParser +# Try to import vector stores with exception handling +try: + from langchain_community.vectorstores import Chroma +except ImportError: + Chroma = None + +try: + from langchain_postgres.vectorstores import PGVector +except ImportError: + PGVector = None +from .embedding_factory import EmbeddingFactory + +from ..core.config import settings +from ..models.message import MessageRole +from ..utils.schemas import ChatResponse, MessageResponse +from ..utils.exceptions import ChatServiceError +from ..utils.logger import get_logger +from .conversation import ConversationService +from .document_processor import get_document_processor + +logger = get_logger("knowledge_chat_service") + + +class KnowledgeChatService: + """Knowledge base chat service using LangChain RAG.""" + + def __init__(self, db: Session): + self.db = db + self.conversation_service = ConversationService(db) + + # 获取当前LLM配置 + llm_config = settings.llm.get_current_config() + + # Initialize LangChain ChatOpenAI + self.llm = ChatOpenAI( + model=llm_config["model"], + api_key=llm_config["api_key"], + base_url=llm_config["base_url"], + temperature=llm_config["temperature"], + max_tokens=llm_config["max_tokens"], + streaming=False + ) + + # Streaming LLM for stream responses + self.streaming_llm = ChatOpenAI( + model=llm_config["model"], + api_key=llm_config["api_key"], + base_url=llm_config["base_url"], + temperature=llm_config["temperature"], + max_tokens=llm_config["max_tokens"], + streaming=True + ) + + # Initialize embeddings based on provider + self.embeddings = EmbeddingFactory.create_embeddings() + + logger.info(f"Knowledge Chat Service initialized with model: {self.llm.model_name}") + + def _get_vector_store(self, knowledge_base_id: int) -> Optional[Any]: + """Get vector store for knowledge base.""" + try: + if settings.vector_db.type == "pgvector": + # 使用PGVector + if not PGVector: + logger.error("PGVector module not available. Cannot use pgvector vector store.") + return None + + doc_processor = get_document_processor() + collection_name = f"{settings.vector_db.pgvector_table_name}_kb_{knowledge_base_id}" + + vector_store = PGVector( + connection=doc_processor.connection_string, + embeddings=self.embeddings, + collection_name=collection_name, + use_jsonb=True + ) + + return vector_store + else: + # 兼容Chroma模式 + if not Chroma: + logger.error("Chroma module not available. Cannot use Chroma vector store.") + return None + + import os + kb_vector_path = os.path.join(get_document_processor().vector_db_path, f"kb_{knowledge_base_id}") + + if not os.path.exists(kb_vector_path): + logger.warning(f"Vector store not found for knowledge base {knowledge_base_id}") + return None + + vector_store = Chroma( + persist_directory=kb_vector_path, + embedding_function=self.embeddings + ) + + return vector_store + + except Exception as e: + logger.error(f"Failed to load vector store for KB {knowledge_base_id}: {str(e)}") + return None + + def _create_rag_chain(self, vector_store, conversation_history: List[Dict[str, str]]): + """Create RAG chain with conversation history.""" + + # Create retriever + retriever = vector_store.as_retriever( + search_type="similarity", + search_kwargs={"k": 5} + ) + + # Create prompt template + system_prompt = """你是一个智能助手,基于提供的上下文信息回答用户问题。 + +上下文信息: +{context} + +请根据上下文信息回答用户的问题。如果上下文信息不足以回答问题,请诚实地说明。 +保持回答准确、有用且简洁。""" + + prompt = ChatPromptTemplate.from_messages([ + ("system", system_prompt), + MessagesPlaceholder(variable_name="chat_history"), + ("human", "{question}") + ]) + + # Create chain + def format_docs(docs): + return "\n\n".join(doc.page_content for doc in docs) + + rag_chain = ( + { + "context": retriever | format_docs, + "question": RunnablePassthrough(), + "chat_history": lambda x: conversation_history + } + | prompt + | self.llm + | StrOutputParser() + ) + + return rag_chain, retriever + + def _prepare_conversation_history(self, messages: List) -> List[Dict[str, str]]: + """Prepare conversation history for RAG chain.""" + history = [] + + for msg in messages[:-1]: # Exclude the last message (current user message) + if msg.role == MessageRole.USER: + history.append({"role": "human", "content": msg.content}) + elif msg.role == MessageRole.ASSISTANT: + history.append({"role": "assistant", "content": msg.content}) + + return history + + async def chat_with_knowledge_base( + self, + conversation_id: int, + message: str, + knowledge_base_id: int, + stream: bool = False, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None + ) -> ChatResponse: + """Chat with knowledge base using RAG.""" + + try: + # Get conversation and validate + conversation = self.conversation_service.get_conversation(conversation_id) + if not conversation: + raise ChatServiceError("Conversation not found") + + # Get vector store + vector_store = self._get_vector_store(knowledge_base_id) + if not vector_store: + raise ChatServiceError(f"Knowledge base {knowledge_base_id} not found or not processed") + + # Save user message + user_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=message, + role=MessageRole.USER + ) + + # Get conversation history + messages = self.conversation_service.get_conversation_messages(conversation_id) + conversation_history = self._prepare_conversation_history(messages) + + # Create RAG chain + rag_chain, retriever = self._create_rag_chain(vector_store, conversation_history) + + # Get relevant documents for context + relevant_docs = retriever.get_relevant_documents(message) + context_documents = [] + + for doc in relevant_docs: + context_documents.append({ + "content": doc.page_content[:500], # Limit content length + "metadata": doc.metadata, + "source": doc.metadata.get("filename", "unknown") + }) + + # Generate response + if stream: + # For streaming, we'll use a different approach + response_content = await self._generate_streaming_response( + rag_chain, message, conversation_id + ) + else: + response_content = await asyncio.to_thread(rag_chain.invoke, message) + + # Save assistant message with context + assistant_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=response_content, + role=MessageRole.ASSISTANT, + context_documents=context_documents + ) + + # Create response + return ChatResponse( + user_message=MessageResponse.from_orm(user_message), + assistant_message=MessageResponse.from_orm(assistant_message), + model_used=self.llm.model_name, + total_tokens=None # TODO: Calculate tokens if needed + ) + + except Exception as e: + logger.error(f"Knowledge base chat failed: {str(e)}") + raise ChatServiceError(f"Knowledge base chat failed: {str(e)}") + + async def _generate_streaming_response( + self, + rag_chain, + message: str, + conversation_id: int + ) -> str: + """Generate streaming response (placeholder for now).""" + # For now, use non-streaming approach + # TODO: Implement proper streaming with RAG chain + return await asyncio.to_thread(rag_chain.invoke, message) + + async def chat_stream_with_knowledge_base( + self, + conversation_id: int, + message: str, + knowledge_base_id: int, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None + ) -> AsyncGenerator[str, None]: + """Chat with knowledge base using RAG with streaming response.""" + + try: + + # Get vector store + vector_store = self._get_vector_store(knowledge_base_id) + if not vector_store: + raise ChatServiceError(f"Knowledge base {knowledge_base_id} not found or not processed") + + # Get conversation history + messages = self.conversation_service.get_conversation_messages(conversation_id) + conversation_history = self._prepare_conversation_history(messages) + + # Create RAG chain + rag_chain, retriever = self._create_rag_chain(vector_store, conversation_history) + + # Save user message + user_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=message, + role=MessageRole.USER + ) + + # Get relevant documents + relevant_docs = retriever.get_relevant_documents(message) + context = "\n\n".join([doc.page_content for doc in relevant_docs]) + + # Create streaming LLM + llm_config = settings.llm.get_current_config() + streaming_llm = ChatOpenAI( + model=llm_config["model"], + temperature=temperature or llm_config["temperature"], + max_tokens=max_tokens or llm_config["max_tokens"], + streaming=True, + api_key=llm_config["api_key"], + base_url=llm_config["base_url"] + ) + + # Create prompt for streaming + prompt = ChatPromptTemplate.from_messages([ + ("system", "你是一个智能助手。请基于以下上下文信息回答用户的问题。如果上下文中没有相关信息,请诚实地说明。\n\n上下文信息:\n{context}"), + MessagesPlaceholder(variable_name="chat_history"), + ("human", "{question}") + ]) + + # Prepare chat history for prompt + chat_history_messages = [] + for hist in conversation_history: + if hist["role"] == "human": + chat_history_messages.append(HumanMessage(content=hist["content"])) + elif hist["role"] == "assistant": + chat_history_messages.append(AIMessage(content=hist["content"])) + + # Create streaming chain + streaming_chain = ( + { + "context": lambda x: context, + "chat_history": lambda x: chat_history_messages, + "question": lambda x: x["question"] + } + | prompt + | streaming_llm + | StrOutputParser() + ) + + # Generate streaming response + full_response = "" + async for chunk in streaming_chain.astream({"question": message}): + if chunk: + full_response += chunk + yield chunk + + # Save assistant response + if full_response: + self.conversation_service.add_message( + conversation_id=conversation_id, + content=full_response, + role=MessageRole.ASSISTANT, + message_metadata={ + "knowledge_base_id": knowledge_base_id, + "relevant_docs_count": len(relevant_docs) + } + ) + + except Exception as e: + logger.error(f"Error in knowledge base streaming chat: {str(e)}") + error_message = f"知识库对话出错: {str(e)}" + yield error_message + + # Save error message + self.conversation_service.add_message( + conversation_id=conversation_id, + content=error_message, + role=MessageRole.ASSISTANT + ) + + async def search_knowledge_base( + self, + knowledge_base_id: int, + query: str, + k: int = 5 + ) -> List[Dict[str, Any]]: + """Search knowledge base for relevant documents.""" + + try: + vector_store = self._get_vector_store(knowledge_base_id) + if not vector_store: + return [] + + # Perform similarity search + results = vector_store.similarity_search_with_score(query, k=k) + + formatted_results = [] + for doc, score in results: + formatted_results.append({ + "content": doc.page_content, + "metadata": doc.metadata, + "similarity_score": float(score), + "source": doc.metadata.get("filename", "unknown") + }) + + return formatted_results + + except Exception as e: + logger.error(f"Knowledge base search failed: {str(e)}") + return [] \ No newline at end of file diff --git a/backend/th_agenter/services/langchain_chat.py b/backend/th_agenter/services/langchain_chat.py new file mode 100644 index 0000000..c04a17f --- /dev/null +++ b/backend/th_agenter/services/langchain_chat.py @@ -0,0 +1,383 @@ +"""LangChain-based chat service.""" + +import json +import asyncio +import os +from typing import AsyncGenerator, Optional, List, Dict, Any +from sqlalchemy.orm import Session + +from langchain_openai import ChatOpenAI +from langchain_core.messages import HumanMessage, AIMessage, SystemMessage +from langchain_core.callbacks import BaseCallbackHandler +from langchain_core.outputs import LLMResult + +from ..core.config import settings +from ..models.message import MessageRole +from ..utils.schemas import ChatResponse, StreamChunk, MessageResponse +from ..utils.exceptions import ChatServiceError, OpenAIError, AuthenticationError, RateLimitError +from ..utils.logger import get_logger +from .conversation import ConversationService + +logger = get_logger("langchain_chat_service") + + +class StreamingCallbackHandler(BaseCallbackHandler): + """Custom callback handler for streaming responses.""" + + def __init__(self): + self.tokens = [] + + def on_llm_new_token(self, token: str, **kwargs) -> None: + """Handle new token from LLM.""" + self.tokens.append(token) + + def get_response(self) -> str: + """Get the complete response.""" + return "".join(self.tokens) + + def clear(self): + """Clear the tokens.""" + self.tokens = [] + + +class LangChainChatService: + """LangChain-based chat service for AI model integration.""" + + def __init__(self, db: Session): + self.db = db + self.conversation_service = ConversationService(db) + + from ..core.llm import create_llm + + # 添加调试日志 + logger.info(f"LLM Provider: {settings.llm.provider}") + + # Initialize LangChain ChatOpenAI + self.llm = create_llm(streaming=False) + + # Streaming LLM for stream responses + self.streaming_llm = create_llm(streaming=True) + + self.streaming_handler = StreamingCallbackHandler() + + logger.info(f"LangChain ChatService initialized with model: {self.llm.model_name}") + + def _prepare_langchain_messages(self, conversation, history: List) -> List: + """Prepare messages for LangChain format.""" + messages = [] + + # Add system message if conversation has system prompt + if hasattr(conversation, 'system_prompt') and conversation.system_prompt: + messages.append(SystemMessage(content=conversation.system_prompt)) + else: + # Default system message + messages.append(SystemMessage( + content="You are a helpful AI assistant. Please provide accurate and helpful responses." + )) + + # Add conversation history + for msg in history[:-1]: # Exclude the last message (current user message) + if msg.role == MessageRole.USER: + messages.append(HumanMessage(content=msg.content)) + elif msg.role == MessageRole.ASSISTANT: + messages.append(AIMessage(content=msg.content)) + + # Add current user message + if history: + last_msg = history[-1] + if last_msg.role == MessageRole.USER: + messages.append(HumanMessage(content=last_msg.content)) + + return messages + + async def chat( + self, + conversation_id: int, + message: str, + stream: bool = False, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None + ) -> ChatResponse: + """Send a message and get AI response using LangChain.""" + logger.info(f"Processing LangChain chat request for conversation {conversation_id}") + + try: + # Get conversation details + conversation = self.conversation_service.get_conversation(conversation_id) + if not conversation: + raise ChatServiceError("Conversation not found") + + # Add user message to database + user_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=message, + role=MessageRole.USER + ) + + # Get conversation history for context + history = self.conversation_service.get_conversation_history( + conversation_id, limit=20 + ) + + # Prepare messages for LangChain + langchain_messages = self._prepare_langchain_messages(conversation, history) + + # Update LLM parameters if provided + llm_to_use = self.llm + if temperature is not None or max_tokens is not None: + llm_config = settings.llm.get_current_config() + llm_to_use = ChatOpenAI( + model=llm_config["model"], + openai_api_key=llm_config["api_key"], + openai_api_base=llm_config["base_url"], + temperature=temperature if temperature is not None else float(conversation.temperature), + max_tokens=max_tokens if max_tokens is not None else conversation.max_tokens, + streaming=False + ) + + # Call LangChain LLM + response = await llm_to_use.ainvoke(langchain_messages) + + # Extract response content + assistant_content = response.content + + # Add assistant message to database + assistant_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=assistant_content, + role=MessageRole.ASSISTANT, + message_metadata={ + "model": llm_to_use.model_name, + "langchain_version": "0.1.0", + "provider": "langchain_openai" + } + ) + + # Update conversation timestamp + self.conversation_service.update_conversation_timestamp(conversation_id) + + logger.info(f"Successfully processed LangChain chat request for conversation {conversation_id}") + + return ChatResponse( + user_message=MessageResponse.from_orm(user_message), + assistant_message=MessageResponse.from_orm(assistant_message), + total_tokens=None, # LangChain doesn't provide token count by default + model_used=llm_to_use.model_name + ) + + except Exception as e: + logger.error(f"Failed to process LangChain chat request for conversation {conversation_id}: {str(e)}", exc_info=True) + + # Classify error types for better handling + error_type = type(e).__name__ + error_message = self._format_error_message(e) + + # Add error message to database + assistant_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=error_message, + role=MessageRole.ASSISTANT, + message_metadata={ + "error": True, + "error_type": error_type, + "original_error": str(e), + "langchain_error": True + } + ) + + # Re-raise specific exceptions for proper error handling + if "rate limit" in str(e).lower(): + raise RateLimitError(str(e)) + elif "api key" in str(e).lower() or "authentication" in str(e).lower(): + raise AuthenticationError(str(e)) + elif "openai" in str(e).lower(): + raise OpenAIError(str(e)) + + return ChatResponse( + user_message=MessageResponse.from_orm(user_message), + assistant_message=MessageResponse.from_orm(assistant_message), + total_tokens=0, + model_used=self.llm.model_name + ) + + async def chat_stream( + self, + conversation_id: int, + message: str, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None + ) -> AsyncGenerator[str, None]: + """Send a message and get streaming AI response using LangChain.""" + logger.info(f"Processing LangChain streaming chat request for conversation {conversation_id}") + + try: + # Get conversation details + conversation = self.conversation_service.get_conversation(conversation_id) + if not conversation: + raise ChatServiceError("Conversation not found") + + # Add user message to database + user_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=message, + role=MessageRole.USER + ) + + # Get conversation history for context + history = self.conversation_service.get_conversation_history( + conversation_id, limit=20 + ) + + # Prepare messages for LangChain + langchain_messages = self._prepare_langchain_messages(conversation, history) + + # Update streaming LLM parameters if provided + streaming_llm_to_use = self.streaming_llm + if temperature is not None or max_tokens is not None: + llm_config = settings.llm.get_current_config() + streaming_llm_to_use = ChatOpenAI( + model=llm_config["model"], + openai_api_key=llm_config["api_key"], + openai_api_base=llm_config["base_url"], + temperature=temperature if temperature is not None else float(conversation.temperature), + max_tokens=max_tokens if max_tokens is not None else conversation.max_tokens, + streaming=True + ) + + # Clear previous streaming handler state + self.streaming_handler.clear() + + # Stream response + full_response = "" + async for chunk in streaming_llm_to_use.astream(langchain_messages): + if hasattr(chunk, 'content') and chunk.content: + full_response += chunk.content + yield chunk.content + + # Add complete assistant message to database + assistant_message = self.conversation_service.add_message( + conversation_id=conversation_id, + content=full_response, + role=MessageRole.ASSISTANT, + message_metadata={ + "model": streaming_llm_to_use.model_name, + "langchain_version": "0.1.0", + "provider": "langchain_openai", + "streaming": True + } + ) + + # Update conversation timestamp + self.conversation_service.update_conversation_timestamp(conversation_id) + + logger.info(f"Successfully processed LangChain streaming chat request for conversation {conversation_id}") + + except Exception as e: + logger.error(f"Failed to process LangChain streaming chat request for conversation {conversation_id}: {str(e)}", exc_info=True) + + # Format error message for user + error_message = self._format_error_message(e) + yield error_message + + # Add error message to database + self.conversation_service.add_message( + conversation_id=conversation_id, + content=error_message, + role=MessageRole.ASSISTANT, + message_metadata={ + "error": True, + "error_type": type(e).__name__, + "original_error": str(e), + "langchain_error": True, + "streaming": True + } + ) + + async def get_available_models(self) -> List[str]: + """Get list of available models from LangChain.""" + try: + # LangChain doesn't have a direct method to list models + # Return commonly available OpenAI models + return [ + "gpt-3.5-turbo", + "gpt-3.5-turbo-16k", + "gpt-4", + "gpt-4-turbo-preview", + "gpt-4o", + "gpt-4o-mini" + ] + except Exception as e: + logger.error(f"Failed to get available models: {str(e)}") + return ["gpt-3.5-turbo"] + + def update_model_config( + self, + model: Optional[str] = None, + temperature: Optional[float] = None, + max_tokens: Optional[int] = None + ): + """Update LLM configuration.""" + from ..core.llm import create_llm + + # 重新创建LLM实例 + self.llm = create_llm( + model=model, + temperature=temperature, + streaming=False + ) + + self.streaming_llm = create_llm( + model=model, + temperature=temperature, + streaming=True + ) + + logger.info(f"Updated LLM configuration: model={model}, temperature={temperature}, max_tokens={max_tokens}") + + def _format_error_message(self, error: Exception) -> str: + """Format error message for user display.""" + error_type = type(error).__name__ + error_str = str(error) + + # Provide user-friendly error messages + if "rate limit" in error_str.lower(): + return "服务器繁忙,请稍后再试。" + elif "api key" in error_str.lower() or "authentication" in error_str.lower(): + return "API认证失败,请检查配置。" + elif "timeout" in error_str.lower(): + return "请求超时,请重试。" + elif "connection" in error_str.lower(): + return "网络连接错误,请检查网络连接。" + elif "model" in error_str.lower() and "not found" in error_str.lower(): + return "指定的模型不可用,请选择其他模型。" + else: + return f"处理请求时发生错误:{error_str}" + + async def _retry_with_backoff(self, func, max_retries: int = 3, base_delay: float = 1.0): + """Retry function with exponential backoff.""" + for attempt in range(max_retries): + try: + return await func() + except Exception as e: + if attempt == max_retries - 1: + raise e + + # Check if error is retryable + if not self._is_retryable_error(e): + raise e + + delay = base_delay * (2 ** attempt) + logger.warning(f"Attempt {attempt + 1} failed, retrying in {delay}s: {str(e)}") + await asyncio.sleep(delay) + + def _is_retryable_error(self, error: Exception) -> bool: + """Check if an error is retryable.""" + error_str = str(error).lower() + retryable_errors = [ + "timeout", + "connection", + "server error", + "internal error", + "rate limit" + ] + return any(err in error_str for err in retryable_errors) \ No newline at end of file diff --git a/backend/th_agenter/services/llm_config_service.py b/backend/th_agenter/services/llm_config_service.py new file mode 100644 index 0000000..0a5b4dd --- /dev/null +++ b/backend/th_agenter/services/llm_config_service.py @@ -0,0 +1,121 @@ +"""LLM配置服务 - 从数据库读取默认配置""" + +from typing import Optional, Dict, Any +from sqlalchemy.orm import Session +from sqlalchemy import and_ + +from ..models.llm_config import LLMConfig +from ..utils.logger import get_logger +from ..db.database import get_db_session + +logger = get_logger("llm_config_service") + + +class LLMConfigService: + """LLM配置管理服务""" + + def __init__(self, db_session: Optional[Session] = None): + self.db = db_session or get_db_session() + + def get_default_chat_config(self) -> Optional[LLMConfig]: + """获取默认对话模型配置""" + try: + config = self.db.query(LLMConfig).filter( + and_( + LLMConfig.is_default == True, + LLMConfig.is_embedding == False, + LLMConfig.is_active == True + ) + ).first() + + if not config: + logger.warning("未找到默认对话模型配置") + return None + + return config + + except Exception as e: + logger.error(f"获取默认对话模型配置失败: {str(e)}") + return None + + def get_default_embedding_config(self) -> Optional[LLMConfig]: + """获取默认嵌入模型配置""" + try: + config = self.db.query(LLMConfig).filter( + and_( + LLMConfig.is_default == True, + LLMConfig.is_embedding == True, + LLMConfig.is_active == True + ) + ).first() + + if not config: + logger.warning("未找到默认嵌入模型配置") + return None + + return config + + except Exception as e: + logger.error(f"获取默认嵌入模型配置失败: {str(e)}") + return None + + def get_config_by_id(self, config_id: int) -> Optional[LLMConfig]: + """根据ID获取配置""" + try: + return self.db.query(LLMConfig).filter(LLMConfig.id == config_id).first() + except Exception as e: + logger.error(f"获取配置失败: {str(e)}") + return None + + def get_active_configs(self, is_embedding: Optional[bool] = None) -> list: + """获取所有激活的配置""" + try: + query = self.db.query(LLMConfig).filter(LLMConfig.is_active == True) + + if is_embedding is not None: + query = query.filter(LLMConfig.is_embedding == is_embedding) + + return query.order_by(LLMConfig.created_at).all() + + except Exception as e: + logger.error(f"获取激活配置失败: {str(e)}") + return [] + + def _get_fallback_chat_config(self) -> Dict[str, Any]: + """获取fallback对话模型配置(从环境变量)""" + from ..core.config import get_settings + settings = get_settings() + return settings.llm.get_current_config() + + def _get_fallback_embedding_config(self) -> Dict[str, Any]: + """获取fallback嵌入模型配置(从环境变量)""" + from ..core.config import get_settings + settings = get_settings() + return settings.embedding.get_current_config() + + def test_config(self, config_id: int, test_message: str = "Hello") -> Dict[str, Any]: + """测试配置连接""" + try: + config = self.get_config_by_id(config_id) + if not config: + return {"success": False, "error": "配置不存在"} + + # 这里可以添加实际的连接测试逻辑 + # 例如发送一个简单的请求来验证配置是否有效 + + return {"success": True, "message": "配置测试成功"} + + except Exception as e: + logger.error(f"测试配置失败: {str(e)}") + return {"success": False, "error": str(e)} + + +# 全局实例 +_llm_config_service = None + +def get_llm_config_service(db_session: Optional[Session] = None) -> LLMConfigService: + """获取LLM配置服务实例""" + global _llm_config_service + if _llm_config_service is None or db_session is not None: + _llm_config_service = LLMConfigService(db_session) + return _llm_config_service \ No newline at end of file diff --git a/backend/th_agenter/services/llm_service.py b/backend/th_agenter/services/llm_service.py new file mode 100644 index 0000000..19b4aad --- /dev/null +++ b/backend/th_agenter/services/llm_service.py @@ -0,0 +1,113 @@ +"""LLM service for workflow execution.""" + +import asyncio +from typing import List, Dict, Any, Optional, AsyncGenerator +from langchain_openai import ChatOpenAI +from langchain_core.messages import HumanMessage, AIMessage, SystemMessage + +from ..models.llm_config import LLMConfig +from ..utils.logger import get_logger + +logger = get_logger("llm_service") + + +class LLMService: + """LLM服务,用于工作流中的大模型调用""" + + def __init__(self): + pass + + async def chat_completion( + self, + model_config: LLMConfig, + messages: List[Dict[str, str]], + temperature: Optional[float] = None, + max_tokens: Optional[int] = None + ) -> str: + """调用大模型进行对话完成""" + try: + # 创建LangChain ChatOpenAI实例 + llm = ChatOpenAI( + model=model_config.model_name, + api_key=model_config.api_key, + base_url=model_config.base_url, + temperature=temperature or model_config.temperature, + max_tokens=max_tokens or model_config.max_tokens, + streaming=False + ) + + # 转换消息格式 + langchain_messages = [] + for msg in messages: + role = msg.get("role", "user") + content = msg.get("content", "") + + if role == "system": + langchain_messages.append(SystemMessage(content=content)) + elif role == "user": + langchain_messages.append(HumanMessage(content=content)) + elif role == "assistant": + langchain_messages.append(AIMessage(content=content)) + + # 调用LLM + response = await llm.ainvoke(langchain_messages) + + # 返回响应内容 + return response.content + + except Exception as e: + logger.error(f"LLM调用失败: {str(e)}") + raise Exception(f"LLM调用失败: {str(e)}") + + async def chat_completion_stream( + self, + model_config: LLMConfig, + messages: List[Dict[str, str]], + temperature: Optional[float] = None, + max_tokens: Optional[int] = None + ) -> AsyncGenerator[str, None]: + """调用大模型进行流式对话完成""" + try: + # 创建LangChain ChatOpenAI实例(流式) + llm = ChatOpenAI( + model=model_config.model_name, + api_key=model_config.api_key, + base_url=model_config.base_url, + temperature=temperature or model_config.temperature, + max_tokens=max_tokens or model_config.max_tokens, + streaming=True + ) + + # 转换消息格式 + langchain_messages = [] + for msg in messages: + role = msg.get("role", "user") + content = msg.get("content", "") + + if role == "system": + langchain_messages.append(SystemMessage(content=content)) + elif role == "user": + langchain_messages.append(HumanMessage(content=content)) + elif role == "assistant": + langchain_messages.append(AIMessage(content=content)) + + # 流式调用LLM + async for chunk in llm.astream(langchain_messages): + if hasattr(chunk, 'content') and chunk.content: + yield chunk.content + + except Exception as e: + logger.error(f"LLM流式调用失败: {str(e)}") + raise Exception(f"LLM流式调用失败: {str(e)}") + + def get_model_info(self, model_config: LLMConfig) -> Dict[str, Any]: + """获取模型信息""" + return { + "id": model_config.id, + "name": model_config.model_name, + "provider": model_config.provider, + "base_url": model_config.base_url, + "temperature": model_config.temperature, + "max_tokens": model_config.max_tokens, + "is_active": model_config.is_active + } \ No newline at end of file diff --git a/backend/th_agenter/services/mcp/__init__.py b/backend/th_agenter/services/mcp/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/th_agenter/services/mcp/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/services/mcp/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fe9809f1e5e2c1aec2a4d2e9bd2af1872782ed3f GIT binary patch literal 173 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 Cl4{EU literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/mcp/__pycache__/postgresql_mcp.cpython-313.pyc b/backend/th_agenter/services/mcp/__pycache__/postgresql_mcp.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..543f8eb85d608e9ab190f4f799ae81c3f85c59f5 GIT binary patch literal 13818 zcmb_DX?RoDl~?;>N#1SA>$9=3Wo%*Au!C((zy;( z^?~oZ%em*Cd*9jaxmP!hMm>S&-oG|_e!iBF&#)jps#xaclaP6iu!JM5jFq>`I2nD* zIXS!)?TR)fr)*Pksx~#JZqslY2H}+L+BPkxrFm7mu1(MB+cLO}HUnp9Gjc{+uWmQB zWpbGe>B+1ltY(!=9Ffa16DVto2&?PSuTsQnv-{;%{Ufb=e7wJ(bF+JQGP_##FtxjU zz3u^~#pm_A5Br%toX_ub`39_v-{%`(`g|PI>-0MhIC(eY8*+0_zsKk0ZPyWJhgB&k zcY1igpxo+l`2|Ij_lTg~J%k|60jo^VLRYunGw23DZ8M+ekIq3GgIU##a zvFpf?ThL=wr<)H9_yLQ)-|cq{`1<<+ICn^-o_}b-G3Xj{_yN!*&QDA;V}+ZCU@`88 zW$z)d+ys}JQ?PPQX(uaT(Tm6fT293(d(P7^DsX-NwuSUmiO0HL9E8LYO) z(4+1#u9RavK$tqqV&f&fI+>(GEW1+4>L`xvc{q|V0+gOY<34hm z%tHY;cf{lEx4B|`PS+N>8~<)LLb6Hr9O2IBxe(RO#C*JzY`KM-1j9mG*7iOLtpE>@<^xMx|`{heB-+AS;qu;o8X6))a z&&<5^_{__%Ts!x#Py?E5=GYIeoq6i&>&K?Q_42iEzKh+TJvZ~zH)c-14Mh>j_2=KX z`uek<9es=|g|Rsl+~lcd;&gi5yo>Wt1twG^$cyo?Ed|7kD726qB~dxa+997mcO$gY zm0t&|3cMNTTvpZ{=fh6?EGZ8wXBA69#q5=_sXc4UWHqdo)vZ+~_0T8v zNH-FwwUi!)JM=Kl>tVV>kIZ>JvXXk>8m2rfC`vYm&0VWnss@#k*CFWQ<|kkP;F)KR zUwQY%t8czD{nEF)tZG3MlUX5S04$ONOxA##7fj+8J>V8A1f%<~+ZFJ;9aNbJnSM9G zCZt`^_j>qP(W(-Zl=YQvA=U<5r~2r9#Y#Q+WBGd*(rFXx9v*tyFi zuIXRS9}{$FVCfnKa99*EDsI=#y*_>7m3a3#tC{Gn8t@Ey{DCUyDR!e0`{MP}Bh$k_ zhef&m&nJM|>+hVp_Qv;@H8x)F1wgAR{ZxD1eNOZPRGbc=ICQrJHQUv>qvLKty}P-s zwWUkY?C9ufy}Pw@o=}@mVuvJGgBO_eGM#`Z2K4+dAUR5|7%Y)|W-5R2WPU>^zadgm z7qOHI`TUIxwb3+dBF3Ufer;0o{Siyq4V?+PEU1^pcc8%71uE^C0swMi+&H8=fD9-Sjx_1IB3PE zlGwpdMZMmQ34QFcPK}gLyir%JieZp^Q0S|~Ztzyc-l9VR`f07&E4Rz5NCoMROTLCU zO9=5Qx{*ryu)6LHDQ{Oucui!5T*|v;tlq0=QuN9C$VNpu>62MA0_D(WdiaIuciv8X z>l8T@lqi9M5+%^H2F#bBm4r{wO2S8-SL*j(N1n5)xy|t6wm>2%5u9tratoF-W8&{X zU)LA#2hcJkh8IO~`ulHQ`{^s|m<~`@+9Cdk%Qw`&oI{xu6yW9v3gA^i*Lv9H7VVFM z_j4fsXkEFD2&GRb8A0aeQI#|Bh?Y9X;o{ujZ6_7vnXV_U{ZwmorpE%VdEGW0uX8VUc$iA?SNOg0G>GSG902}A-#K029IPEHcLmwjL zk>G$b`R&_TrCn)Pu_{(=Q&NdTKb$@wap0rT+j%po_&!)~zFx}HM?@yN$?Iu&{veA*l@!m;OW7xQo{#a;862|CN`2_ zIOyaZL;Yf3K{E)A;C^qLhe8xbtAg7LxCIr+vybDtAm3rtaUEEvb_F=z#|fI4l@qjm zZoliG&+8VH2Vmn8D?0}U1eI&R2P&Zrx^eB0ST#v zp(Mj23Uiht)i0?`Bg!c-NIU;CNKlZAbf!pd;bd-ID7S8GbvSqV@YYDa<@xTD-Oqew zcw1CWjJYQ|j&%f!ZI=v7BLyYT`%e181vO)`P(j`B-AV0xFBv+gETyOVUiO{#h0B(P zEseu%mkl{5b{^aLR7b>+b)xN9+fzHCM+p-tu8c5sk>aIb2J=i&0=DoL7{e^fO`_77 zE*CRXmXcF@UnoEbjWW;lIZ+x-pa7up3m*OP$C~q-&E$QvzInak{Yq2wYQ_7FGR&`5 zK{`hb;F?i2Ag5{o3{x5 z8-5;KKBo>=M^8B{DT&GAym|ylxQ+F2u)m-BuJ8Ac~4~ z_L!B~(m6%$7Y_ffF~YZ!Y|j})w@lFE=>z#17;T+`C5-(R^>o}d|KhA2{=1ikmDc==Mo%YkxGAU?ZD;U4)ePy{%N zR}HgCzd9Lqxrwlb6-lZDAaA2UVqS^HE(Z@lZP&0y8?NVVybe@3yqtkQud0uL^0@m#La(nmV?|6yKN7ln`w2K*-)PuV0X@ zhm9?@xe&F(1?L6B5a$_maz`BS6?j1hmJ_~3!0U&7_gve{xq;od{g_bet4*@Lh*AW{ z@G{qr$ze>6Ktdz6xUTq!#yB$;@QsHD)5LsJK%PQOrNDXfjq)m8F&a>6lno_WQ8URc z9Vwl)7>rD`gcMqa@1AR$3!gWgG({FI9)0M{>PSV^=-x90QfaYutl~^xq;bV~)0^su zwSH{x*@8%Q?O67idv0WE8L-=0$!?otu6&f}V9XpCGnj3i&D9x~L<`8$Rp)$XeV1&T z!^K;EYuh}^kIG)$7P4&~-We?3lJN3NmyfR-ub*&2xXv$ zV7Zk#_w|v|#iDZ$@vmIbMfk737L$@D*{x_%yh`-upKjXRQcd2^HnlLy_sbh0{{f@I zv^uBdE(Ny|KMa~i7pfBJL1jXniT?rD?2gWa5}k?KSYPK7Fy7!qq%+i#30@Ch&IzH0 z^nN}Y4=sVIg|Gmm@)b%5Af$IN;?UsDD?8(%ENLo)#3_TIRC;ccLc+|>_liVLy5+hqbd*;pP&*0`4C@?h2uq(GcN&|C^XXuiPa?Tb5sG?1#xiV2=>e~#O=d&dPqU{NIE^N_pa;(gET7Zsa&x$Q7IYroCvE{nosQ~U(85rn;x{EJ_PDMP2UgN!3!I>!q@-c| zlB#>!-_eV=5o8`gb;vmYhsH;tmB+XjRsHcqv;iiFKqVgur8y#)3-K$&=1yw?th(Zv z%)C>DW4d#hXEVdKYr~eck@DKf@};5jrQ?cl`O09~s_}gj_KCaSTNW&79nr-M#;LWJ z4Ar1!TVy+Alg(Z5e{A*Sl8wJzvN2q+3DVJ(r<$I9I0Q8Xn}YZC&&S(brq*iZ`|>6O6h6plDu&mG zn+hQPs9J?-T~6x;1y>CnxMi3uhs1@fCOx=Y`X>C{JOE~95h#?DQxG&fDGwMFS@M|! zC?UARvkv?ZC!7b^<&uKyg+qeGISH#^m2hqgXB-fnPNpD+-5xg`UQMDG?9s#>sm88J zx3f#!@PRWF1>lykI!ej6k|}Y*cnPC8k1oAP7mb^zF&0UbX+ed`yK(ZJqWU$VX|U_M8wblC;8seLj85gI>*44 zu%(SMk=A7Tp}ss#y@F*#-6pOaoW|m@C4Jy*hyjk)Ct*>r+1J| zcfqN&wJLxvr08uWLcR2|6TQ6>pe`A`yOgeZ9mCZ@|7|q1fA##^)8i+_Loo3o$@GcG zAkuG@Q)i8{17sp6oom5uFuX-6wk`&c0Itb=Tr}$Ha&SZKpX(?!+|I0 z^mox9>A7^WE2{&mRrs0hpMbJ00=M(F;2BCqkLy%EWQ;s@m0m zc|7?_(>f+l2}tL%g?KqmoVRqH0I%7)54+&%VxN!FkrpUNP{(f2D(SfkT7C`;3*kU} z5v~Ih^iu?d*Y_~@Bo=W_+(Aqp$D|FDc1#|@Cgp<5)5>o;t*Mu$DL{1SH}$X*Xb@puq4(+y4eu(;%A{3|DgiHk`SSZ zYIn(XrYQ)@v>$5^7OfsXGX7xD+Q3IT$fFBDd zDLGLC$tgbN31`;;+MjDA#4X8)&DjN!it5RVHKB?%!L@DSiuRGV%ZnQ$l`E!d8_yYj zXn4&uS-T}vyCq!P449V{jBK6Es|w{+jqVHOHGsQZRykSP5GrjLdmvo8c4XTXq`vBM z&8A50+KIYQZEIw4{nQfM*rC_fgM-Z!-B4+9Qd*c)8B#YG&R+88!m`nt$z@HUWliDA zCW*|hWHJ-O^oL4UkAE#xx^+Yk7ogy3NXTLxs|#CJji|5SnlP6uYo}`9l=*vKr3h}Q za2r6>upC)1V6e;<&bHlg#J}l@xl*EJ^_?jxt(Yub7AjpfUKcLiF`^eKi4<2uPaItV zqw8_HzHl~t<2y_j2d|I@Uv=lf%NI?SFAtSN)a+t)xO~ruK^nd$b@+jBcHN(Ii%vZ} zS+_1!w=P`1E-KH+FN&B;Ce5`WbM07R$h>U4K4jiJX>JXfTQ8aKh83MGsSlOZkL?SV zteS$zTUBkOsv)>^M{rRa;Ae_pdX}QuJW{e4zI=qS2A8x3%eN)ShBJ3_WuyeYjmYsu z%?Yw@&HCgp*_*(w1+f`3wjc51%U(M)b}(3W7sc{9$^ab91I*uN?~#*|YJkr#qO<@x zYtH9R?0u(TLL01Xq67iFr3m5ki~c@aDINHu+`%DYuz2Hn*LxN3^iAv!TDArE+J|?B z4EIdsmPX9*zy3$;c=l`ejBO7V;KJjj00mwOP{E~u|K>4h^O$VsQrY9S_Db?&%>ASR zau*oPUZ^O={3gu5PnsIrwB& z`Cn=3F~5T5S0c=>Etvmx5yJesj^>x(z!%M!zi27J{OUDrtH{STtoe9p2Go4Kj^@`J zApeO5^Pgz-ZOav(b-l8RS3B zN61epC7%|lu%;X#Kdsd7tXF(mziMZV;x`pC$p5BN1!MiDMuqu$1>~(Vs=wj28Wf^c z)d7!~Ex~*4XaM1!yWZg#^z{Y?FmG_ceNX2=tR>Uo=<`5aXTalidwtNXaX5N?E{B6& zQWxz2`p_I2DGt;v5wB;i#~j9yIP{{pyCD(u@hd*OsQsz6p~i=r7iv$oBRDlGYRpml zgaIWXAMV@x;g*04Tn`8{9e~Tba0Op5#%|-g;O7Aj2h0;~JiX7oMYIzbI^!@wCxhP& zjCAWs@(1Pa&y{jjSyU@iF{c*I5_m<`GSv?GXf76Z$fN37m3~w?`jrr~VwONY+9flp znq*^nW3KTPV+YTcMG2NCwoh)@9on#aa>IS04foB`x@cL6$~5X9YZ=cS+kWQjQ3Az@ zstNwRmWji^u+3sIy1}d}23)=4D<%}Y^*xsEWs3Q39`t-uG5qRJ^z5 z7v5Pc-tf!np{*rK;Psw@zAp9#*?WTfyMt`^$KKDeA=)E@>*WqvWKUP51(d;FIncsK z3BD%|UsPUXgPrz^`#&~?Ht!Q_BV~(6`O||@1%gDjY`>U$!6+6kGqq#6XN*yb>6KOe?x=#5{>4? str: + return "mysql_mcp" + + def get_description(self) -> str: + return "MySQL MCP服务工具,提供数据库连接、表结构查询、SQL执行等功能,支持智能数据问答。" + + def get_parameters(self) -> List[ToolParameter]: + return [ + ToolParameter( + name="operation", + type=ToolParameterType.STRING, + description="操作类型", + required=True, + enum=["connect", "list_tables", "describe_table", "execute_query", "test_connection", "disconnect"] + ), + ToolParameter( + name="connection_config", + type=ToolParameterType.OBJECT, + description="数据库连接配置 {host, port, database, username, password}", + required=False + ), + ToolParameter( + name="user_id", + type=ToolParameterType.STRING, + description="用户ID,用于管理连接", + required=False + ), + ToolParameter( + name="table_name", + type=ToolParameterType.STRING, + description="表名(用于describe_table操作)", + required=False + ), + ToolParameter( + name="sql_query", + type=ToolParameterType.STRING, + description="SQL查询语句(用于execute_query操作)", + required=False + ), + ToolParameter( + name="limit", + type=ToolParameterType.INTEGER, + description="查询结果限制数量,默认100", + required=False, + default=100 + ) + ] + + def _get_tables(self, connection) -> List[Dict[str, Any]]: + """获取数据库表列表""" + cursor = connection.cursor() + try: + cursor.execute(""" + SELECT + table_name, + table_type, + table_schema + FROM information_schema.tables + WHERE table_schema = DATABASE() + ORDER BY table_name; + """) + + tables = [] + for row in cursor.fetchall(): + tables.append({ + "table_name": row[0], + "table_type": row[1], + "table_schema": row[2] + }) + + return tables + finally: + cursor.close() + + def _describe_table(self, connection, table_name: str) -> Dict[str, Any]: + """获取表结构信息""" + cursor = connection.cursor() + try: + # 获取列信息 + cursor.execute(""" + SELECT + column_name, + data_type, + is_nullable, + column_default, + character_maximum_length, + numeric_precision, + numeric_scale, + column_comment + FROM information_schema.columns + WHERE table_schema = DATABASE() AND table_name = %s + ORDER BY ordinal_position; + """, (table_name,)) + + columns = [] + for row in cursor.fetchall(): + column_info = { + "column_name": row[0], + "data_type": row[1], + "is_nullable": row[2] == 'YES', + "column_default": row[3], + "character_maximum_length": row[4], + "numeric_precision": row[5], + "numeric_scale": row[6], + "column_comment": row[7] or "" + } + columns.append(column_info) + + # 获取主键信息 + cursor.execute(""" + SELECT column_name + FROM information_schema.key_column_usage + WHERE table_schema = DATABASE() + AND table_name = %s + AND constraint_name = 'PRIMARY' + ORDER BY ordinal_position; + """, (table_name,)) + + primary_keys = [row[0] for row in cursor.fetchall()] + + # 获取外键信息 + cursor.execute(""" + SELECT + column_name, + referenced_table_name, + referenced_column_name + FROM information_schema.key_column_usage + WHERE table_schema = DATABASE() + AND table_name = %s + AND referenced_table_name IS NOT NULL; + """, (table_name,)) + + foreign_keys = [] + for row in cursor.fetchall(): + foreign_keys.append({ + "column_name": row[0], + "referenced_table": row[1], + "referenced_column": row[2] + }) + + # 获取索引信息 + cursor.execute(""" + SELECT + index_name, + column_name, + non_unique + FROM information_schema.statistics + WHERE table_schema = DATABASE() + AND table_name = %s + ORDER BY index_name, seq_in_index; + """, (table_name,)) + + indexes = [] + for row in cursor.fetchall(): + indexes.append({ + "index_name": row[0], + "column_name": row[1], + "is_unique": row[2] == 0 + }) + + # 获取表注释 + cursor.execute(""" + SELECT table_comment + FROM information_schema.tables + WHERE table_schema = DATABASE() AND table_name = %s; + """, (table_name,)) + + table_comment = "" + result = cursor.fetchone() + if result: + table_comment = result[0] or "" + + return { + "table_name": table_name, + "columns": columns, + "primary_keys": primary_keys, + "foreign_keys": foreign_keys, + "indexes": indexes, + "table_comment": table_comment + } + + finally: + cursor.close() + + def _execute_query(self, connection, sql_query: str, limit: int = 100) -> Dict[str, Any]: + """执行SQL查询""" + cursor = connection.cursor() + try: + # 添加LIMIT限制(如果查询中没有LIMIT) + if limit and limit > 0 and "LIMIT" not in sql_query.upper(): + sql_query = f"{sql_query.rstrip(';')} LIMIT {limit}" + + cursor.execute(sql_query) + + # 获取列名 + columns = [desc[0] for desc in cursor.description] if cursor.description else [] + + # 获取数据 + rows = cursor.fetchall() + + # 转换为字典列表 + data = [] + for row in rows: + row_dict = {} + for i, value in enumerate(row): + if i < len(columns): + # 处理特殊数据类型 + if isinstance(value, datetime): + row_dict[columns[i]] = value.isoformat() + else: + row_dict[columns[i]] = value + data.append(row_dict) + return { + "success": True, + "data": data, + "columns": columns, + "row_count": len(data), + "query": sql_query + } + + finally: + cursor.close() + + def _create_connection(self, config: Dict[str, Any]) -> pymysql.Connection: + """创建MySQL数据库连接""" + try: + connection = pymysql.connect( + host=config['host'], + port=int(config.get('port', 3306)), + user=config['username'], + password=config['password'], + database=config['database'], + connect_timeout=10, + charset='utf8mb4' + ) + return connection + except Exception as e: + raise Exception(f"MySQL连接失败: {str(e)}") + + def _test_connection(self, config: Dict[str, Any]) -> Dict[str, Any]: + """测试数据库连接""" + try: + conn = self._create_connection(config) + cursor = conn.cursor() + + # 获取数据库版本信息 + cursor.execute("SELECT VERSION();") + version = cursor.fetchone()[0] + + # 获取数据库引擎信息 + cursor.execute("SHOW ENGINES;") + engines = cursor.fetchall() + has_innodb = any('InnoDB' in str(engine) for engine in engines) + + cursor.close() + conn.close() + + return { + "success": True, + "version": version, + "has_innodb": has_innodb, + "message": "连接测试成功" + } + except Exception as e: + return { + "success": False, + "error": str(e), + "message": "连接测试失败" + } + + + + async def execute(self, **kwargs) -> ToolResult: + """Execute the MySQL MCP tool operation.""" + try: + operation = kwargs.get("operation") + connection_config = kwargs.get("connection_config", {}) + user_id = kwargs.get("user_id") + table_name = kwargs.get("table_name") + sql_query = kwargs.get("sql_query") + limit = kwargs.get("limit", 100) + + logger.info(f"执行MySQL MCP操作: {operation}") + if operation == "test_connection": + if not connection_config: + return ToolResult( + success=False, + error="缺少连接配置参数" + ) + + result = self._test_connection(connection_config) + return ToolResult( + success=result["success"], + result=result, + error=result.get("error") + ) + elif operation == "connect": + if not connection_config: + return ToolResult( + success=False, + error="缺少connection_config参数" + ) + + if not user_id: + return ToolResult( + success=False, + error="缺少user_id参数" + ) + + try: + # 建立MySQL连接 + connection = pymysql.connect( + host=connection_config["host"], + port=int(connection_config["port"]), + user=connection_config["username"], + password=connection_config["password"], + database=connection_config["database"], + charset='utf8mb4', + cursorclass=pymysql.cursors.Cursor + ) + + # 存储连接 + self.connections[user_id] = { + "connection": connection, + "config": connection_config, + "connected_at": datetime.now().isoformat() + } + + # 获取表列表 + tables = self._get_tables(connection) + + return ToolResult( + success=True, + result={ + "message": "数据库连接成功", + "database": connection_config["database"], + "tables": tables, + "table_count": len(tables) + } + ) + except Exception as e: + return ToolResult( + success=False, + error=f"连接失败: {str(e)}" + ) + + elif operation == "list_tables": + if not user_id or user_id not in self.connections: + return ToolResult( + success=False, + error="用户未连接数据库,请先执行connect操作" + ) + + connection = self.connections[user_id]["connection"] + tables = self._get_tables(connection) + + return ToolResult( + success=True, + result={ + "tables": tables, + "table_count": len(tables) + } + ) + + elif operation == "describe_table": + if not user_id or user_id not in self.connections: + return ToolResult( + success=False, + error="用户未连接数据库,请先执行connect操作" + ) + + if not table_name: + return ToolResult( + success=False, + error="缺少table_name参数" + ) + + connection = self.connections[user_id]["connection"] + table_info = self._describe_table(connection, table_name) + + return ToolResult( + success=True, + result=table_info + ) + + elif operation == "execute_query": + if not user_id or user_id not in self.connections: + return ToolResult( + success=False, + error="用户未连接数据库,请先执行connect操作" + ) + + if not sql_query: + return ToolResult( + success=False, + error="缺少sql_query参数" + ) + + connection = self.connections[user_id]["connection"] + query_result = self._execute_query(connection, sql_query, limit) + + return ToolResult( + success=True, + result=query_result + ) + + elif operation == "disconnect": + if user_id and user_id in self.connections: + try: + self.connections[user_id]["connection"].close() + del self.connections[user_id] + return ToolResult( + success=True, + result={"message": "数据库连接已断开"} + ) + except Exception as e: + return ToolResult( + success=False, + error=f"断开连接失败: {str(e)}" + ) + else: + return ToolResult( + success=True, + result={"message": "用户未连接数据库"} + ) + + else: + return ToolResult( + success=False, + result=f"不支持的操作类型: {operation}", + ) + + except Exception as e: + logger.error(f"MySQL MCP工具执行失败: {str(e)}", exc_info=True) + return ToolResult( + success=False, + error=f"工具执行失败: {str(e)}" + ) \ No newline at end of file diff --git a/backend/th_agenter/services/mcp/postgresql_mcp.py b/backend/th_agenter/services/mcp/postgresql_mcp.py new file mode 100644 index 0000000..7cf56c8 --- /dev/null +++ b/backend/th_agenter/services/mcp/postgresql_mcp.py @@ -0,0 +1,389 @@ +"""PostgreSQL MCP (Model Context Protocol) tool for database operations.""" + +import json +import psycopg2 +from typing import List, Dict, Any, Optional +from datetime import datetime + +from th_agenter.services.agent.base import BaseTool, ToolParameter, ToolParameterType, ToolResult +from th_agenter.utils.logger import get_logger + +logger = get_logger("postgresql_mcp_tool") + + +class PostgreSQLMCPTool(BaseTool): + """PostgreSQL MCP tool for database operations and intelligent querying.""" + + def __init__(self): + super().__init__() + self.connections = {} # 存储用户的数据库连接 + + def get_name(self) -> str: + return "postgresql_mcp" + + def get_description(self) -> str: + return "PostgreSQL MCP服务工具,提供数据库连接、表结构查询、SQL执行等功能,支持智能数据问答。" + + def get_parameters(self) -> List[ToolParameter]: + return [ + ToolParameter( + name="operation", + type=ToolParameterType.STRING, + description="操作类型", + required=True, + enum=["connect", "list_tables", "describe_table", "execute_query", "test_connection", "disconnect"] + ), + ToolParameter( + name="connection_config", + type=ToolParameterType.OBJECT, + description="数据库连接配置 {host, port, database, username, password}", + required=False + ), + ToolParameter( + name="user_id", + type=ToolParameterType.STRING, + description="用户ID,用于管理连接", + required=False + ), + ToolParameter( + name="table_name", + type=ToolParameterType.STRING, + description="表名(用于describe_table操作)", + required=False + ), + ToolParameter( + name="sql_query", + type=ToolParameterType.STRING, + description="SQL查询语句(用于execute_query操作)", + required=False + ), + ToolParameter( + name="limit", + type=ToolParameterType.INTEGER, + description="查询结果限制数量,默认100", + required=False, + default=100 + ) + ] + + def _create_connection(self, config: Dict[str, Any]) -> psycopg2.extensions.connection: + """创建PostgreSQL数据库连接""" + try: + connection = psycopg2.connect( + host=config['host'], + port=int(config.get('port', 5432)), + user=config['username'], + password=config['password'], + database=config['database'], + connect_timeout=10 + ) + return connection + except Exception as e: + raise Exception(f"PostgreSQL连接失败: {str(e)}") + + def _test_connection(self, config: Dict[str, Any]) -> Dict[str, Any]: + """测试数据库连接""" + try: + conn = self._create_connection(config) + cursor = conn.cursor() + + # 获取数据库版本信息 + cursor.execute("SELECT version();") + version = cursor.fetchone()[0] + + # 检查pgvector扩展 + cursor.execute("SELECT * FROM pg_extension WHERE extname = 'vector';") + has_vector = bool(cursor.fetchall()) + + cursor.close() + conn.close() + + return { + "success": True, + "version": version, + "has_pgvector": has_vector, + "message": "连接测试成功" + } + except Exception as e: + return { + "success": False, + "error": str(e), + "message": "连接测试失败" + } + + def _get_tables(self, connection) -> List[Dict[str, Any]]: + """获取数据库表列表""" + cursor = connection.cursor() + try: + cursor.execute(""" + SELECT + table_name, + table_type, + table_schema + FROM information_schema.tables + WHERE table_schema = 'public' + ORDER BY table_name; + """) + + tables = [] + for row in cursor.fetchall(): + tables.append({ + "table_name": row[0], + "table_type": row[1], + "table_schema": row[2] + }) + + return tables + finally: + cursor.close() + + def _describe_table(self, connection, table_name: str) -> Dict[str, Any]: + """获取表结构信息""" + cursor = connection.cursor() + try: + # 获取列信息 + cursor.execute(""" + SELECT + column_name, + data_type, + is_nullable, + column_default, + character_maximum_length, + numeric_precision, + numeric_scale + FROM information_schema.columns + WHERE table_name = %s AND table_schema = 'public' + ORDER BY ordinal_position; + """, (table_name,)) + + columns = [] + for row in cursor.fetchall(): + columns.append({ + "column_name": row[0], + "data_type": row[1], + "is_nullable": row[2], + "column_default": row[3], + "character_maximum_length": row[4], + "numeric_precision": row[5], + "numeric_scale": row[6] + }) + + # 获取主键信息 + cursor.execute(""" + SELECT column_name + FROM information_schema.key_column_usage + WHERE table_name = %s AND table_schema = 'public' + AND constraint_name IN ( + SELECT constraint_name + FROM information_schema.table_constraints + WHERE table_name = %s AND constraint_type = 'PRIMARY KEY' + ); + """, (table_name, table_name)) + + primary_keys = [row[0] for row in cursor.fetchall()] + + # 获取表行数 + cursor.execute(f"SELECT COUNT(*) FROM {table_name};") + row_count = cursor.fetchone()[0] + + return { + "table_name": table_name, + "columns": columns, + "primary_keys": primary_keys, + "row_count": row_count + } + finally: + cursor.close() + + def _execute_query(self, connection, sql_query: str, limit: int = 100) -> Dict[str, Any]: + """执行SQL查询""" + cursor = connection.cursor() + try: + # 添加LIMIT限制(如果查询中没有) + if limit and "LIMIT" not in sql_query.upper(): + sql_query = f"{sql_query.rstrip(';')} LIMIT {limit};" + + cursor.execute(sql_query) + + # 获取列名 + columns = [desc[0] for desc in cursor.description] if cursor.description else [] + + # 获取结果 + if cursor.description: # SELECT查询 + rows = cursor.fetchall() + data = [] + for row in rows: + row_dict = {} + for i, value in enumerate(row): + if i < len(columns): + # 处理特殊数据类型 + if isinstance(value, datetime): + row_dict[columns[i]] = value.isoformat() + else: + row_dict[columns[i]] = value + data.append(row_dict) + + return { + "success": True, + "data": data, + "columns": columns, + "row_count": len(data), + "query": sql_query + } + else: # INSERT/UPDATE/DELETE查询 + affected_rows = cursor.rowcount + return { + "success": True, + "affected_rows": affected_rows, + "query": sql_query, + "message": f"查询执行成功,影响 {affected_rows} 行" + } + finally: + cursor.close() + + async def execute(self, operation: str, connection_config: Optional[Dict[str, Any]] = None, + user_id: Optional[str] = None, table_name: Optional[str] = None, + sql_query: Optional[str] = None, limit: int = 100) -> ToolResult: + """执行PostgreSQL MCP操作""" + try: + logger.info(f"执行PostgreSQL MCP操作: {operation}") + + if operation == "test_connection": + if not connection_config: + return ToolResult( + success=False, + error="缺少连接配置参数" + ) + + result = self._test_connection(connection_config) + return ToolResult( + success=result["success"], + result=result, + error=result.get("error") + ) + + elif operation == "connect": + if not connection_config or not user_id: + return ToolResult( + success=False, + error="缺少连接配置或用户ID参数" + ) + + try: + connection = self._create_connection(connection_config) + self.connections[user_id] = { + "connection": connection, + "config": connection_config, + "connected_at": datetime.now().isoformat() + } + + # 获取表列表 + tables = self._get_tables(connection) + + return ToolResult( + success=True, + result={ + "message": "数据库连接成功", + "database": connection_config["database"], + "tables": tables, + "table_count": len(tables) + } + ) + except Exception as e: + return ToolResult( + success=False, + error=f"连接失败: {str(e)}" + ) + + elif operation == "list_tables": + if not user_id or user_id not in self.connections: + return ToolResult( + success=False, + error="用户未连接数据库,请先执行connect操作" + ) + + connection = self.connections[user_id]["connection"] + tables = self._get_tables(connection) + + return ToolResult( + success=True, + result={ + "tables": tables, + "table_count": len(tables) + } + ) + + elif operation == "describe_table": + if not user_id or user_id not in self.connections: + return ToolResult( + success=False, + error="用户未连接数据库,请先执行connect操作" + ) + + if not table_name: + return ToolResult( + success=False, + error="缺少table_name参数" + ) + + connection = self.connections[user_id]["connection"] + table_info = self._describe_table(connection, table_name) + + return ToolResult( + success=True, + result=table_info + ) + + elif operation == "execute_query": + if not user_id or user_id not in self.connections: + return ToolResult( + success=False, + error="用户未连接数据库,请先执行connect操作" + ) + + if not sql_query: + return ToolResult( + success=False, + error="缺少sql_query参数" + ) + + connection = self.connections[user_id]["connection"] + query_result = self._execute_query(connection, sql_query, limit) + + return ToolResult( + success=True, + result=query_result + ) + + elif operation == "disconnect": + if user_id and user_id in self.connections: + try: + self.connections[user_id]["connection"].close() + del self.connections[user_id] + return ToolResult( + success=True, + result={"message": "数据库连接已断开"} + ) + except Exception as e: + return ToolResult( + success=False, + error=f"断开连接失败: {str(e)}" + ) + else: + return ToolResult( + success=True, + result={"message": "用户未连接数据库"} + ) + + else: + return ToolResult( + success=False, + error=f"不支持的操作类型: {operation}" + ) + + except Exception as e: + logger.error(f"PostgreSQL MCP工具执行失败: {str(e)}", exc_info=True) + return ToolResult( + success=False, + error=f"工具执行失败: {str(e)}" + ) \ No newline at end of file diff --git a/backend/th_agenter/services/mysql_tool_manager.py b/backend/th_agenter/services/mysql_tool_manager.py new file mode 100644 index 0000000..21b7977 --- /dev/null +++ b/backend/th_agenter/services/mysql_tool_manager.py @@ -0,0 +1,51 @@ +"""MySQL MCP工具全局管理器""" + +from typing import Optional +from ..utils.logger import get_logger + +logger = get_logger("mysql_tool_manager") + +# Try to import MySQL MCP tool +try: + from th_agenter.services.mcp.mysql_mcp import MySQLMCPTool + MYSQL_TOOL_AVAILABLE = True +except ImportError as e: + logger.warning(f"MySQL MCP tool not available: {str(e)}. MySQL functionality will be disabled.") + MySQLMCPTool = None + MYSQL_TOOL_AVAILABLE = False + + +class MySQLToolManager: + """MySQL工具全局单例管理器""" + + _instance: Optional['MySQLToolManager'] = None + _mysql_tool: Optional[MySQLMCPTool] = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + @property + def mysql_tool(self) -> Optional[MySQLMCPTool]: + """获取MySQL工具实例""" + if not MYSQL_TOOL_AVAILABLE: + return None + + if self._mysql_tool is None: + self._mysql_tool = MySQLMCPTool() + logger.info("创建全局MySQL工具实例") + return self._mysql_tool + + def get_tool(self) -> Optional[MySQLMCPTool]: + """获取MySQL工具实例(别名方法)""" + return self.mysql_tool + + +# 全局实例 +mysql_tool_manager = MySQLToolManager() + + +def get_mysql_tool() -> MySQLMCPTool: + """获取全局MySQL工具实例""" + return mysql_tool_manager.get_tool() \ No newline at end of file diff --git a/backend/th_agenter/services/postgresql_tool_manager.py b/backend/th_agenter/services/postgresql_tool_manager.py new file mode 100644 index 0000000..feccbe0 --- /dev/null +++ b/backend/th_agenter/services/postgresql_tool_manager.py @@ -0,0 +1,51 @@ +"""PostgreSQL MCP工具全局管理器""" + +from typing import Optional +from ..utils.logger import get_logger + +logger = get_logger("postgresql_tool_manager") + +# Try to import PostgreSQL MCP tool +try: + from th_agenter.services.mcp.postgresql_mcp import PostgreSQLMCPTool + POSTGRESQL_TOOL_AVAILABLE = True +except ImportError as e: + logger.warning(f"PostgreSQL MCP tool not available: {str(e)}. PostgreSQL functionality will be disabled.") + PostgreSQLMCPTool = None + POSTGRESQL_TOOL_AVAILABLE = False + + +class PostgreSQLToolManager: + """PostgreSQL工具全局单例管理器""" + + _instance: Optional['PostgreSQLToolManager'] = None + _postgresql_tool: Optional[PostgreSQLMCPTool] = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + @property + def postgresql_tool(self) -> Optional[PostgreSQLMCPTool]: + """获取PostgreSQL工具实例""" + if not POSTGRESQL_TOOL_AVAILABLE: + return None + + if self._postgresql_tool is None: + self._postgresql_tool = PostgreSQLMCPTool() + logger.info("创建全局PostgreSQL工具实例") + return self._postgresql_tool + + def get_tool(self) -> Optional[PostgreSQLMCPTool]: + """获取PostgreSQL工具实例(别名方法)""" + return self.postgresql_tool + + +# 全局实例 +postgresql_tool_manager = PostgreSQLToolManager() + + +def get_postgresql_tool() -> PostgreSQLMCPTool: + """获取全局PostgreSQL工具实例""" + return postgresql_tool_manager.get_tool() \ No newline at end of file diff --git a/backend/th_agenter/services/smart_db_workflow.py b/backend/th_agenter/services/smart_db_workflow.py new file mode 100644 index 0000000..8975b3a --- /dev/null +++ b/backend/th_agenter/services/smart_db_workflow.py @@ -0,0 +1,879 @@ +from typing import Dict, Any, List, Optional +import logging +from datetime import datetime +import asyncio +from concurrent.futures import ThreadPoolExecutor +from langchain_openai import ChatOpenAI +from th_agenter.core.context import UserContext +from .smart_query import DatabaseQueryService +from .postgresql_tool_manager import get_postgresql_tool +from .mysql_tool_manager import get_mysql_tool +from .table_metadata_service import TableMetadataService +from ..core.config import get_settings + +# 配置日志 +logger = logging.getLogger(__name__) + +class SmartWorkflowError(Exception): + """智能工作流自定义异常""" + pass + +class DatabaseConnectionError(SmartWorkflowError): + """数据库连接异常""" + pass + +class TableSchemaError(SmartWorkflowError): + """表结构获取异常""" + pass + +class SQLGenerationError(SmartWorkflowError): + """SQL生成异常""" + pass + +class QueryExecutionError(SmartWorkflowError): + """查询执行异常""" + pass + + +class SmartDatabaseWorkflowManager: + """ + 智能数据库工作流管理器 + 负责协调数据库连接、表元数据获取、SQL生成、查询执行和AI总结的完整流程 + """ + + def __init__(self, db=None): + self.executor = ThreadPoolExecutor(max_workers=4) + self.database_service = DatabaseQueryService() + self.postgresql_tool = get_postgresql_tool() + self.mysql_tool = get_mysql_tool() + self.db = db + self.table_metadata_service = TableMetadataService(db) if db else None + + from ..core.llm import create_llm + self.llm = create_llm() + + def _get_database_tool(self, db_type: str): + """根据数据库类型获取对应的数据库工具""" + if db_type.lower() == 'postgresql': + return self.postgresql_tool + elif db_type.lower() == 'mysql': + return self.mysql_tool + else: + raise ValueError(f"不支持的数据库类型: {db_type}") + + async def _run_in_executor(self, func, *args): + """在线程池中运行阻塞函数""" + loop = asyncio.get_event_loop() + return await loop.run_in_executor(self.executor, func, *args) + + def _convert_query_result_to_table_data(self, query_result: Dict[str, Any]) -> Dict[str, Any]: + """ + 将数据库查询结果转换为前端表格数据格式 + 参考Excel处理方式,以表格形式返回结果 + """ + try: + data = query_result.get('data', []) + columns = query_result.get('columns', []) + row_count = query_result.get('row_count', 0) + + if not data or not columns: + return { + 'result_type': 'table', + 'columns': [], + 'data': [], + 'total': 0, + 'message': '查询未返回数据' + } + + # 构建列定义 + table_columns = [] + for i, col_name in enumerate(columns): + table_columns.append({ + 'prop': f'col_{i}', + 'label': str(col_name), + 'width': 'auto' + }) + + # 转换数据行 + table_data = [] + for row_index, row in enumerate(data): + row_data = {'_index': str(row_index)} + # 处理字典格式的行数据 + if isinstance(row, dict): + for i, col_name in enumerate(columns): + col_prop = f'col_{i}' + value = row.get(col_name) + # 处理None值和特殊值 + if value is None: + row_data[col_prop] = '' + elif isinstance(value, (int, float, str, bool)): + row_data[col_prop] = str(value) + else: + row_data[col_prop] = str(value) + else: + # 处理列表格式的行数据(兼容性处理) + for i, value in enumerate(row): + col_prop = f'col_{i}' + # 处理None值和特殊值 + if value is None: + row_data[col_prop] = '' + elif isinstance(value, (int, float, str, bool)): + row_data[col_prop] = str(value) + else: + row_data[col_prop] = str(value) + + table_data.append(row_data) + + return { + 'result_type': 'table_data', + 'columns': table_columns, + 'data': table_data, + 'total': row_count, + 'message': f'查询成功,共返回 {row_count} 条记录' + } + + except Exception as e: + logger.error(f"转换查询结果异常: {str(e)}") + return { + 'result_type': 'error', + 'columns': [], + 'data': [], + 'total': 0, + 'message': f'结果转换失败: {str(e)}' + } + + async def process_database_query_stream( + self, + user_query: str, + user_id: int, + database_config_id: int + ): + """ + 流式处理数据库智能问数查询的主要工作流(基于保存的表元数据) + 实时推送每个工作流步骤 + + 新流程: + 1. 根据database_config_id获取数据库配置并创建连接 + 2. 从系统数据库读取表元数据(只包含启用问答的表) + 3. 根据表元数据生成SQL + 4. 执行SQL查询 + 5. 查询数据后处理成表格形式 + 6. 生成数据总结 + 7. 返回结果 + + Args: + user_query: 用户问题 + user_id: 用户ID + database_config_id: 数据库配置ID + + Yields: + 包含工作流步骤或最终结果的字典 + """ + workflow_steps = [] + + try: + logger.info(f"开始执行流式数据库查询工作流 - 用户ID: {user_id}, 数据库配置ID: {database_config_id}, 查询: {user_query[:50]}...") + + # 步骤1: 根据database_config_id获取数据库配置并创建连接 + try: + step_data = { + 'type': 'workflow_step', + 'step': 'database_connection', + 'status': 'running', + 'message': '正在建立数据库连接...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + # 获取数据库配置并建立连接 + connection_result = await self._connect_database(user_id, database_config_id) + if not connection_result['success']: + raise DatabaseConnectionError(connection_result['message']) + + step_data.update({ + 'status': 'completed', + 'message': '数据库连接成功', + 'details': {'database': connection_result.get('database_name', 'Unknown')} + }) + yield step_data + + workflow_steps.append({ + 'step': 'database_connection', + 'status': 'completed', + 'message': '数据库连接成功' + }) + + except Exception as e: + error_msg = f'数据库连接失败: {str(e)}' + step_data = { + 'type': 'workflow_step', + 'step': 'database_connection', + 'status': 'failed', + 'message': error_msg, + 'timestamp': datetime.now().isoformat() + } + yield step_data + + yield { + 'type': 'error', + 'message': error_msg, + 'workflow_steps': workflow_steps + } + return + + # 步骤2: 从系统数据库读取表元数据(只包含启用问答的表) + try: + step_data = { + 'type': 'workflow_step', + 'step': 'table_metadata', + 'status': 'running', + 'message': '正在从系统数据库读取表元数据...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + # 从系统数据库读取已保存的表元数据(只包含启用问答的表) + tables_info = await self._get_saved_tables_metadata(user_id, database_config_id) + + step_data.update({ + 'status': 'completed', + 'message': f'成功读取 {len(tables_info)} 个启用问答的表元数据', + 'details': {'table_count': len(tables_info), 'tables': list(tables_info.keys())} + }) + yield step_data + + workflow_steps.append({ + 'step': 'table_metadata', + 'status': 'completed', + 'message': f'成功读取表元数据' + }) + + except Exception as e: + error_msg = f'获取表元数据失败: {str(e)}' + step_data = { + 'type': 'workflow_step', + 'step': 'table_metadata', + 'status': 'failed', + 'message': error_msg, + 'timestamp': datetime.now().isoformat() + } + yield step_data + + yield { + 'type': 'error', + 'message': error_msg, + 'workflow_steps': workflow_steps + } + return + + # 步骤3: 根据表元数据生成SQL + try: + step_data = { + 'type': 'workflow_step', + 'step': 'sql_generation', + 'status': 'running', + 'message': '正在根据表元数据生成SQL查询...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + # 根据表元数据选择相关表并生成SQL + target_tables, target_schemas = await self._select_target_table(user_query, tables_info) + step_data = { + 'type': 'workflow_step', + 'step': 'table_selected', + 'status': 'completed', + 'message': f'已经智能选择了相关表: {", ".join(target_tables)}', + 'timestamp': datetime.now().isoformat() + } + + yield step_data + workflow_steps.append({ + 'step': 'table_metadata', + 'status': 'completed', + 'message': f'已经智能选择了相关表: {", ".join(target_tables)}', + }) + sql_query = await self._generate_sql_query(user_query, target_tables, target_schemas) + + step_data.update({ + 'status': 'completed', + 'message': 'SQL查询生成成功', + 'details': { + 'target_tables': target_tables, + 'generated_sql': sql_query[:100] + '...' if len(sql_query) > 100 else sql_query + } + }) + yield step_data + + workflow_steps.append({ + 'step': 'sql_generation', + 'status': 'completed', + 'message': 'SQL语句生成成功' + }) + + except Exception as e: + error_msg = f'SQL生成失败: {str(e)}' + step_data = { + 'type': 'workflow_step', + 'step': 'sql_generation', + 'status': 'failed', + 'message': error_msg, + 'timestamp': datetime.now().isoformat() + } + yield step_data + + yield { + 'type': 'error', + 'message': error_msg, + 'workflow_steps': workflow_steps + } + return + + # 步骤4: 执行SQL查询 + try: + step_data = { + 'type': 'workflow_step', + 'step': 'query_execution', + 'status': 'running', + 'message': '正在执行SQL查询...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + query_result = await self._execute_database_query(user_id, sql_query, database_config_id) + + step_data.update({ + 'status': 'completed', + 'message': f'查询执行成功,返回 {query_result.get("row_count", 0)} 条记录', + 'details': {'row_count': query_result.get('row_count', 0)} + }) + yield step_data + + workflow_steps.append({ + 'step': 'query_execution', + 'status': 'completed', + 'message': '查询执行成功' + }) + + except Exception as e: + error_msg = f'查询执行失败: {str(e)}' + step_data = { + 'type': 'workflow_step', + 'step': 'query_execution', + 'status': 'failed', + 'message': error_msg, + 'timestamp': datetime.now().isoformat() + } + yield step_data + + yield { + 'type': 'error', + 'message': error_msg, + 'workflow_steps': workflow_steps + } + return + + # 步骤5: 查询数据后处理成表格形式(在步骤6中完成) + # 步骤6: 生成数据总结 + try: + step_data = { + 'type': 'workflow_step', + 'step': 'ai_summary', + 'status': 'running', + 'message': '正在生成查询结果总结...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + summary = await self._generate_database_summary(user_query, query_result, ', '.join(target_tables)) + + step_data.update({ + 'status': 'completed', + 'message': '总结生成完成', + 'details': { + 'tables_analyzed': target_tables, + 'summary_length': len(summary) + } + }) + yield step_data + + workflow_steps.append({ + 'step': 'ai_summary', + 'status': 'completed', + 'message': '总结生成完成' + }) + + except Exception as e: + logger.warning(f'生成总结失败: {str(e)}') + summary = '查询执行完成,但生成总结时出现问题。' + + workflow_steps.append({ + 'step': 'ai_summary', + 'status': 'warning', + 'message': '总结生成失败,但查询成功' + }) + + # 步骤7: 返回最终结果,且结果参考excel的处理方式,尽量以表格形式返回 + try: + step_data = { + 'type': 'workflow_step', + 'step': 'result_formatting', + 'status': 'running', + 'message': '正在格式化查询结果...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + # 转换为表格格式 + table_data = self._convert_query_result_to_table_data(query_result) + + step_data.update({ + 'status': 'completed', + 'message': '结果格式化完成' + }) + yield step_data + + workflow_steps.append({ + 'step': 'result_formatting', + 'status': 'completed', + 'message': '结果格式化完成' + }) + + # 返回最终结果 + final_result = { + 'type': 'final_result', + 'success': True, + 'data': { + **table_data, + 'generated_sql': sql_query, + 'summary': summary, + 'table_name': target_tables, + 'query_result': query_result, + 'metadata_source': 'saved_database' # 标记元数据来源 + }, + 'workflow_steps': workflow_steps, + 'timestamp': datetime.now().isoformat() + } + + yield final_result + logger.info(f"数据库查询工作流完成 - 用户ID: {user_id}") + + except Exception as e: + error_msg = f'结果格式化失败: {str(e)}' + yield { + 'type': 'error', + 'message': error_msg, + 'workflow_steps': workflow_steps + } + return + + except Exception as e: + logger.error(f"数据库查询工作流异常: {str(e)}", exc_info=True) + yield { + 'type': 'error', + 'message': f'系统异常: {str(e)}', + 'workflow_steps': workflow_steps + } + + async def _connect_database(self, user_id: int, database_config_id: int) -> Dict[str, Any]: + """连接数据库(判断用户现有连接)""" + try: + # 获取数据库配置 + from ..services.database_config_service import DatabaseConfigService + config_service = DatabaseConfigService(self.db) + config = config_service.get_config_by_id(database_config_id, user_id) + + if not config: + return {'success': False, 'message': '数据库配置不存在'} + + # 根据数据库类型选择对应的工具 + try: + db_tool = self._get_database_tool(config.db_type) + except ValueError as e: + return {'success': False, 'message': str(e)} + + # 测试连接(如果已经有连接则直接复用) + connection_config = { + 'host': config.host, + 'port': config.port, + 'database': config.database, + 'username': config.username, + 'password': config_service._decrypt_password(config.password) + } + + try: + connection = db_tool._test_connection(connection_config) + if connection['success'] == True: + return { + 'success': True, + 'database_name': config.database, + 'db_type': config.db_type, + 'message': '连接成功' + } + else: + return { + 'success': False, + 'database_name': config.database, + 'db_type': config.db_type, + 'message': '连接失败' + } + except Exception as e: + return { + 'success': False, + 'message': f'连接失败: {str(e)}' + } + + except Exception as e: + logger.error(f"数据库连接异常: {str(e)}") + return {'success': False, 'message': f'连接异常: {str(e)}'} + + async def _get_saved_tables_metadata(self, user_id: int, database_config_id: int) -> Dict[str, Dict[str, Any]]: + """从系统数据库中读取已保存的表元数据""" + try: + if not self.table_metadata_service: + raise TableSchemaError("表元数据服务未初始化") + + # 从数据库中获取表元数据 + saved_metadata = self.table_metadata_service.get_user_table_metadata( + user_id, database_config_id + ) + + if not saved_metadata: + raise TableSchemaError(f"未找到数据库配置ID {database_config_id} 的表元数据,请先在数据库管理页面收集表元数据") + + # 转换为所需格式 + tables_metadata = {} + for meta in saved_metadata: + # 只处理启用问答的表 + if meta.is_enabled_for_qa: + tables_metadata[meta.table_name] = { + 'table_name': meta.table_name, + 'columns': meta.columns_info or [], + 'primary_keys': meta.primary_keys or [], + 'row_count': meta.row_count or 0, + 'table_comment': meta.table_comment or '', + 'qa_description': meta.qa_description or '', + 'business_context': meta.business_context or '', + 'from_saved_metadata': True # 标记来源 + } + + if not tables_metadata: + raise TableSchemaError("没有启用问答的表,请在数据库管理页面启用相关表的问答功能") + + logger.info(f"从系统数据库读取表元数据成功,共 {len(tables_metadata)} 个启用问答的表") + return tables_metadata + + except Exception as e: + logger.error(f"读取保存的表元数据异常: {str(e)}") + raise TableSchemaError(f'读取表元数据失败: {str(e)}') + + async def _get_table_schema(self, user_id: int, table_name: str, database_config_id: int) -> Dict[str, Any]: + """获取指定表结构""" + try: + # 获取数据库配置 + from ..services.database_config_service import DatabaseConfigService + config_service = DatabaseConfigService(self.db) + config = config_service.get_config_by_id(database_config_id, user_id) + + if not config: + raise TableSchemaError('数据库配置不存在') + + # 根据数据库类型选择对应的工具 + try: + db_tool = self._get_database_tool(config.db_type) + except ValueError as e: + raise TableSchemaError(str(e)) + + # 使用对应的数据库工具获取表结构 + schema_result = await db_tool.describe_table(table_name) + + if schema_result.get('success'): + return schema_result.get('schema', {}) + else: + raise TableSchemaError(schema_result.get('error', '获取表结构失败')) + + except Exception as e: + logger.error(f"获取表结构异常: {str(e)}") + raise TableSchemaError(f'获取表结构失败: {str(e)}') + + async def _select_target_table(self, user_query: str, tables_info: Dict[str, Dict]) -> tuple[List[str], List[Dict]]: + """根据用户查询选择相关的表,支持返回多个表""" + try: + if len(tables_info) == 1: + # 只有一个表,直接返回 + table_name = list(tables_info.keys())[0] + return [table_name], [tables_info[table_name]] + + # 多个表时,使用LLM选择相关的表 + tables_summary = [] + for table_name, schema in tables_info.items(): + columns = schema.get('columns', []) + column_names = [col.get('column_name', col.get('name', '')) for col in columns] + qa_desc = schema.get('qa_description', '') + business_ctx = schema.get('business_context', '') + tables_summary.append(f"表名: {table_name}\n字段: {', '.join(column_names[:10])}\n表描述: {qa_desc}\n业务上下文: {business_ctx}") + + prompt = f""" + 用户查询: {user_query} + + 可用的表: + {chr(10).join(tables_summary)} + + 请根据用户查询选择相关的表,可以选择多个表。分析表之间可能的关联关系,返回所有相关的表名,用逗号分隔。 + 可以通过qa_description(表描述),business_context(表的业务上下文),以及column_names几个字段判断要使用哪些表。 + 注意:只返回表名列表,后面不要跟其他的内容。 + 例如直接输出: table1,table2,table3 + """ + + response = await self.llm.ainvoke(prompt) + selected_tables = [t.strip() for t in response.content.strip().split(',')] + + # 验证选择的表是否存在 + valid_tables = [] + valid_schemas = [] + for table in selected_tables: + if table in tables_info: + valid_tables.append(table) + valid_schemas.append(tables_info[table]) + else: + logger.warning(f"LLM选择的表 {table} 不存在") + + if valid_tables: + return valid_tables, valid_schemas + else: + # 如果没有有效的表,选择第一个表 + table_name = list(tables_info.keys())[0] + logger.warning(f"没有找到有效的表,使用默认表 {table_name}") + return [table_name], [tables_info[table_name]] + + except Exception as e: + logger.error(f"选择目标表异常: {str(e)}") + # 出现异常时选择第一个表 + table_name = list(tables_info.keys())[0] + return [table_name], [tables_info[table_name]] + + async def _generate_sql_query(self, user_query: str, table_names: List[str], table_schemas: List[Dict]) -> str: + """生成SQL语句,支持多表关联查询""" + try: + # 构建所有表的结构信息 + tables_info = [] + for table_name, schema in zip(table_names, table_schemas): + columns_info = [] + for col in schema.get('columns', []): + col_info = f"{col['column_name']} ({col['data_type']})" + columns_info.append(col_info) + + table_info = f"表名: {table_name}\n" + table_info += f"表描述: {schema.get('qa_description', '')}\n" + table_info += f"业务上下文: {schema.get('business_context', '')}\n" + table_info += "字段信息:\n" + "\n".join(columns_info) + tables_info.append(table_info) + + schema_text = "\n\n".join(tables_info) + + prompt = f""" + 基于以下表结构,将自然语言查询转换为SQL语句。如果需要关联多个表,请分析表之间的关系,使用合适的JOIN语法: + + {schema_text} + + 用户查询: {user_query} + + 请生成对应的SQL查询语句,要求: + 1. 只返回SQL语句,不要包含其他解释 + 2. 如果查询涉及多个表,需要正确处理表之间的关联关系 + 3. 使用合适的JOIN类型(INNER JOIN、LEFT JOIN等) + 4. 确保SELECT的字段来源明确,必要时使用表名前缀 + """ + + # 使用LLM生成SQL + response = await self.llm.ainvoke(prompt) + sql_query = response.content.strip() + + # 清理SQL语句 + if sql_query.startswith('```sql'): + sql_query = sql_query[6:] + if sql_query.endswith('```'): + sql_query = sql_query[:-3] + + sql_query = sql_query.strip() + + logger.info(f"生成的SQL查询: {sql_query}") + return sql_query + + except Exception as e: + logger.error(f"SQL生成异常: {str(e)}") + raise SQLGenerationError(f'SQL生成失败: {str(e)}') + + async def _execute_database_query(self, user_id: int, sql_query: str, database_config_id: int) -> Dict[str, Any]: + """执行SQL语句""" + try: + # 获取数据库配置 + from ..services.database_config_service import DatabaseConfigService + config_service = DatabaseConfigService(self.db) + config = config_service.get_config_by_id(database_config_id, user_id) + + if not config: + raise QueryExecutionError('数据库配置不存在') + + # 根据数据库类型选择对应的工具 + try: + db_tool = self._get_database_tool(config.db_type) + except ValueError as e: + raise QueryExecutionError(str(e)) + + # 使用对应的数据库工具执行查询 + if str(user_id) in db_tool.connections: + query_result = db_tool._execute_query(db_tool.connections[str(user_id)]['connection'], sql_query) + else: + raise QueryExecutionError('请重新进行数据库连接') + + if query_result.get('success'): + data = query_result.get('data', []) + return { + 'success': True, + 'data': data, + 'row_count': len(data), + 'columns': query_result.get('columns', []), + 'sql_query': sql_query + } + else: + raise QueryExecutionError(query_result.get('error', '查询执行失败')) + + except Exception as e: + logger.error(f"查询执行异常: {str(e)}") + raise QueryExecutionError(f'查询执行失败: {str(e)}') + + async def _generate_database_summary(self, user_query: str, query_result: Dict, tables_str: str) -> str: + """生成AI总结,支持多表查询结果""" + try: + data = query_result.get('data', []) + row_count = query_result.get('row_count', 0) + columns = query_result.get('columns', []) + sql_query = query_result.get('sql_query', '') + + # 构建总结提示词 + prompt = f""" +用户查询: {user_query} +涉及的表: {tables_str} +查询结果: 共 {row_count} 条记录 +查询的字段: {', '.join(columns)} +执行的SQL: {sql_query} + +前几条数据示例: +{str(data[:3]) if data else '无数据'} + +请基于以上信息,用中文生成一个简洁的查询结果总结,包括: +1. 查询涉及的表及其关系 +2. 查询的主要发现和数据特征 +3. 如果有关联查询,说明关联的结果特点 +4. 最后对用户的问题进行回答 + +总结要求: +1. 语言简洁明了 +2. 重点突出查询结果 +3. 如果是多表查询,需要说明表之间的关系 +4. 总结不超过300字 +""" + + # 使用LLM生成总结 + response = await self.llm.ainvoke(prompt) + summary = response.content.strip() + + logger.info(f"生成的总结: {summary[:100]}...") + return summary + + except Exception as e: + logger.error(f"总结生成异常: {str(e)}") + return f"查询完成,共返回 {query_result.get('row_count', 0)} 条记录。涉及的表: {tables_str}" + + async def process_database_query( + self, + user_query: str, + user_id: int, + database_config_id: int, + table_name: Optional[str] = None, + conversation_id: Optional[int] = None, + is_new_conversation: bool = False + ) -> Dict[str, Any]: + """ + 处理数据库智能问数查询的主要工作流(基于保存的表元数据) + + 新流程: + 1. 根据database_config_id获取数据库配置 + 2. 创建数据库连接 + 3. 从系统数据库读取表元数据(只包含启用问答的表) + 4. 根据表元数据生成SQL + 5. 执行SQL查询 + 6. 查询数据后处理成表格形式 + 7. 生成数据总结 + 8. 返回结果 + + Args: + user_query: 用户问题 + user_id: 用户ID + database_config_id: 数据库配置ID + table_name: 表名(可选) + conversation_id: 对话ID + is_new_conversation: 是否为新对话 + + Returns: + 包含查询结果的字典 + """ + try: + logger.info(f"开始执行数据库查询工作流 - 用户ID: {user_id}, 数据库配置ID: {database_config_id}, 查询: {user_query[:50]}...") + + # 步骤1: 根据database_config_id获取数据库配置并创建连接 + connection_result = await self._connect_database(user_id, database_config_id) + if not connection_result['success']: + raise DatabaseConnectionError(connection_result['message']) + + logger.info("数据库连接成功") + + # 步骤2: 从系统数据库读取表元数据(只包含启用问答的表) + tables_info = await self._get_saved_tables_metadata(user_id, database_config_id) + + logger.info(f"表元数据读取完成 - 共{len(tables_info)}个启用问答的表") + + # 步骤3: 根据表元数据选择相关表并生成SQL + target_tables, target_schemas = await self._select_target_table(user_query, tables_info) + sql_query = await self._generate_sql_query(user_query, target_tables, target_schemas) + + logger.info(f"SQL生成完成 - 目标表: {', '.join(target_tables)}") + + # 步骤4: 执行SQL查询 + query_result = await self._execute_database_query(user_id, sql_query, database_config_id) + logger.info("查询执行完成") + + # 步骤5: 查询数据后处理成表格形式 + table_data = self._convert_query_result_to_table_data(query_result) + + # 步骤6: 生成数据总结 + summary = await self._generate_database_summary(user_query, query_result, ', '.join(target_tables)) + + # 步骤7: 返回结果 + return { + 'success': True, + 'data': { + **table_data, + 'generated_sql': sql_query, + 'summary': summary, + 'table_names': target_tables, + 'query_result': query_result, + 'metadata_source': 'saved_database' # 标记元数据来源 + } + } + + except SmartWorkflowError as e: + logger.error(f"数据库工作流异常: {str(e)}") + return { + 'success': False, + 'error': str(e), + 'error_type': type(e).__name__ + } + except Exception as e: + logger.error(f"数据库工作流未知异常: {str(e)}", exc_info=True) + return { + 'success': False, + 'error': f'系统异常: {str(e)}', + 'error_type': 'SystemError' + } \ No newline at end of file diff --git a/backend/th_agenter/services/smart_excel_workflow.py b/backend/th_agenter/services/smart_excel_workflow.py new file mode 100644 index 0000000..afb2f47 --- /dev/null +++ b/backend/th_agenter/services/smart_excel_workflow.py @@ -0,0 +1,1391 @@ +from typing import Dict, Any, List, Optional, Union +import pandas as pd +import os +import tempfile +import json +import logging +from datetime import datetime +import asyncio +from concurrent.futures import ThreadPoolExecutor +# Try to import langchain_core modules +try: + from langchain_core.runnables import RunnableLambda + from langchain_core.messages import HumanMessage + from langchain_core.prompts import PromptTemplate + LANGCHAIN_CORE_AVAILABLE = True +except ImportError: + RunnableLambda = None + HumanMessage = None + PromptTemplate = None + LANGCHAIN_CORE_AVAILABLE = False + +# Try to import langchain_experimental modules +try: + from langchain_experimental.agents import create_pandas_dataframe_agent + LANGCHAIN_EXPERIMENTAL_AVAILABLE = True +except ImportError: + create_pandas_dataframe_agent = None + LANGCHAIN_EXPERIMENTAL_AVAILABLE = False + +# Try to import langchain_community modules +try: + from langchain_community.chat_models import ChatZhipuAI + LANGCHAIN_COMMUNITY_AVAILABLE = True +except ImportError: + ChatZhipuAI = None + LANGCHAIN_COMMUNITY_AVAILABLE = False + +# Try to import langchain_openai modules +try: + from langchain_openai import ChatOpenAI + LANGCHAIN_OPENAI_AVAILABLE = True +except ImportError: + ChatOpenAI = None + LANGCHAIN_OPENAI_AVAILABLE = False + +# Try to import langchain_classic modules +try: + from langchain_classic.chains import LLMChain + LANGCHAIN_CLASSIC_AVAILABLE = True +except ImportError: + LLMChain = None + LANGCHAIN_CLASSIC_AVAILABLE = False +from th_agenter.core.context import UserContext +from .smart_query import ExcelAnalysisService +from .excel_metadata_service import ExcelMetadataService +from ..core.config import get_settings +from pathlib import Path + +# 配置日志 +logger = logging.getLogger(__name__) + +class SmartWorkflowError(Exception): + """智能工作流自定义异常""" + pass + +class FileLoadError(SmartWorkflowError): + """文件加载异常""" + pass + +class FileSelectionError(SmartWorkflowError): + """文件选择异常""" + pass + +class CodeExecutionError(SmartWorkflowError): + """代码执行异常""" + pass + + +class SmartExcelWorkflowManager: + """ + 智能工作流管理器 + 负责协调文件选择、代码生成和执行的完整流程 + """ + + def __init__(self, db=None): + self.executor = ThreadPoolExecutor(max_workers=4) + self.excel_service = ExcelAnalysisService() + self.db = db + if db: + self.metadata_service = ExcelMetadataService(db) + else: + self.metadata_service = None + + from ..core.llm import create_llm + # 禁用流式响应,避免pandas代理兼容性问题 + self.llm = create_llm(streaming=False) + + async def _run_in_executor(self, func, *args): + """在线程池中运行阻塞函数""" + loop = asyncio.get_event_loop() + return await loop.run_in_executor(self.executor, func, *args) + + def _convert_dataframe_to_markdown(self, df_string: str) -> str: + """ + 将DataFrame的字符串表示转换为Markdown表格格式 + + Args: + df_string: DataFrame的字符串表示 + + Returns: + Markdown格式的表格字符串 + """ + try: + lines = df_string.strip().split('\n') + + # 查找表格数据的开始位置 + table_start = -1 + for i, line in enumerate(lines): + if '|' in line or (len(line.split()) > 1 and any(char.isdigit() for char in line)): + table_start = i + break + + if table_start == -1: + return df_string # 如果找不到表格,返回原始字符串 + + # 提取表格行 + table_lines = [] + for line in lines[table_start:]: + if line.strip() and not line.startswith('Name:') and not line.startswith('dtype:'): + table_lines.append(line.strip()) + + if not table_lines: + return df_string + + # 处理第一行作为表头 + if table_lines: + # 检查是否已经是表格格式 + if '|' in table_lines[0]: + # 已经是表格格式,直接返回 + markdown_lines = [] + for i, line in enumerate(table_lines): + if i == 1 and not line.startswith('|'): + # 添加分隔行 + cols = table_lines[0].count('|') + 1 + separator = '|' + '---|' * (cols - 1) + '---|' + markdown_lines.append(separator) + markdown_lines.append(line) + return '\n'.join(markdown_lines) + else: + # 转换为Markdown表格格式 + markdown_lines = [] + + # 处理表头 + if len(table_lines) > 0: + # 假设第一行是索引和数据的混合 + first_line = table_lines[0] + parts = first_line.split() + + if len(parts) > 1: + # 创建表头 + header = '| 索引 | ' + ' | '.join(parts[1:]) + ' |' + markdown_lines.append(header) + + # 创建分隔行 + separator = '|' + '---|' * len(parts) + '' + markdown_lines.append(separator) + + # 处理数据行 + for line in table_lines[1:]: + if line.strip(): + parts = line.split() + if len(parts) > 0: + row = '| ' + ' | '.join(parts) + ' |' + markdown_lines.append(row) + + if markdown_lines: + return '\n'.join(markdown_lines) + + return df_string # 如果转换失败,返回原始字符串 + + except Exception as e: + logger.warning(f"DataFrame转Markdown失败: {str(e)}") + return df_string # 转换失败时返回原始字符串 + + def _convert_dataframe_to_markdown(self, df_string: str) -> str: + """ + 将DataFrame的字符串表示转换为Markdown表格格式 + + Args: + df_string: DataFrame的字符串表示 + + Returns: + Markdown格式的表格字符串 + """ + try: + lines = df_string.strip().split('\n') + + # 查找表格数据的开始位置 + table_start = -1 + for i, line in enumerate(lines): + if '|' in line or (len(line.split()) > 1 and any(char.isdigit() for char in line)): + table_start = i + break + + if table_start == -1: + return df_string # 如果找不到表格,返回原始字符串 + + # 提取表格行 + table_lines = [] + for line in lines[table_start:]: + if line.strip() and not line.startswith('Name:') and not line.startswith('dtype:'): + table_lines.append(line.strip()) + + if not table_lines: + return df_string + + # 处理第一行作为表头 + if table_lines: + # 检查是否已经是表格格式 + if '|' in table_lines[0]: + # 已经是表格格式,直接返回 + markdown_lines = [] + for i, line in enumerate(table_lines): + if i == 1 and not line.startswith('|'): + # 添加分隔行 + cols = table_lines[0].count('|') + 1 + separator = '|' + '---|' * (cols - 1) + '---|' + markdown_lines.append(separator) + markdown_lines.append(line) + return '\n'.join(markdown_lines) + else: + # 转换为Markdown表格格式 + markdown_lines = [] + + # 处理表头 + if len(table_lines) > 0: + # 假设第一行是索引和数据的混合 + first_line = table_lines[0] + parts = first_line.split() + + if len(parts) > 1: + # 创建表头 + header = '| 索引 | ' + ' | '.join(parts[1:]) + ' |' + markdown_lines.append(header) + + # 创建分隔行 + separator = '|' + '---|' * len(parts) + '' + markdown_lines.append(separator) + + # 处理数据行 + for line in table_lines[1:]: + if line.strip(): + parts = line.split() + if len(parts) > 0: + row = '| ' + ' | '.join(parts) + ' |' + markdown_lines.append(row) + + if markdown_lines: + return '\n'.join(markdown_lines) + + return df_string # 如果转换失败,返回原始字符串 + + except Exception as e: + logger.warning(f"DataFrame转Markdown失败: {str(e)}") + return df_string # 转换失败时返回原始字符串 + + def _convert_dataframe_to_table_data(self, df: pd.DataFrame) -> Dict[str, Any]: + """ + 将DataFrame转换为前端Table组件可用的结构化数据 + + Args: + df: pandas DataFrame + + Returns: + 包含columns和data的字典 + """ + try: + # 获取列信息 + columns = [] + for col in df.columns: + columns.append({ + 'prop': str(col), + 'label': str(col), + 'width': 'auto' + }) + + # 获取数据 + data = [] + for index, row in df.iterrows(): + row_data = {'_index': str(index)} + for col in df.columns: + # 处理各种数据类型 + value = row[col] + if pd.isna(value): + row_data[str(col)] = '' + elif isinstance(value, (int, float)): + row_data[str(col)] = value + else: + row_data[str(col)] = str(value) + data.append(row_data) + + return { + 'columns': columns, + 'data': data, + 'total': len(df) + } + + except Exception as e: + logger.warning(f"DataFrame转Table数据失败: {str(e)}") + return { + 'columns': [{'prop': 'result', 'label': '结果'}], + 'data': [{'result': str(df)}], + 'total': 1 + } + + async def process_excel_query_stream( + self, + user_query: str, + user_id: int, + conversation_id: Optional[int] = None, + is_new_conversation: bool = False + ): + """ + 流式处理智能问数查询的主要工作流 + 实时推送每个工作流步骤 + + Args: + user_query: 用户问题 + user_id: 用户ID + conversation_id: 对话ID + is_new_conversation: 是否为新对话 + + Yields: + 包含工作流步骤或最终结果的字典 + """ + workflow_steps = [] + + try: + logger.info(f"开始执行流式智能查询工作流 - 用户ID: {user_id}, 查询: {user_query[:50]}...") + + # 步骤1: 加载文件列表 + try: + step_data = { + 'type': 'workflow_step', + 'step': 'file_loading', + 'status': 'running', + 'message': '正在加载用户文件列表...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + if is_new_conversation or conversation_id is None: + file_list = await self._load_user_file_list(user_id) + if not file_list: + raise FileLoadError('未找到可用的Excel文件,请先上传文件') + else: + file_list = await self._load_user_file_list(user_id) + + step_completed = { + 'type': 'workflow_step', + 'step': 'file_loading', + 'status': 'completed', + 'message': f'成功加载{len(file_list)}个文件', + 'details': {'file_count': len(file_list)}, + 'timestamp': datetime.now().isoformat() + } + workflow_steps.append(step_completed) + yield step_completed + logger.info(f"文件加载完成 - 共{len(file_list)}个文件") + + except FileLoadError as e: + step_failed = { + 'type': 'workflow_step', + 'step': 'file_loading', + 'status': 'failed', + 'message': str(e), + 'timestamp': datetime.now().isoformat() + } + workflow_steps.append(step_failed) + yield step_failed + + yield { + 'type': 'final_result', + 'success': False, + 'message': str(e), + 'workflow_steps': workflow_steps + } + return + + # 步骤2: 智能文件选择 + try: + step_data = { + 'type': 'workflow_step', + 'step': 'file_selection', + 'status': 'running', + 'message': '正在分析问题并选择相关文件...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + selected_files = await self._select_relevant_files(user_query, file_list) + + if not selected_files: + raise FileSelectionError('未找到与问题相关的Excel文件') + selected_files_names = names_str = ", ".join([file["filename"] for file in selected_files]) + step_completed = { + 'type': 'workflow_step', + 'step': 'file_selection', + 'status': 'completed', + 'message': f'选择了{len(selected_files)}个相关文件:{selected_files_names}', + 'details': {'selection_count': len(selected_files)}, + 'timestamp': datetime.now().isoformat() + } + workflow_steps.append(step_completed) + yield step_completed + logger.info(f"文件选择完成 - 选择了{len(selected_files)}个文件") + + except FileSelectionError as e: + step_failed = { + 'type': 'workflow_step', + 'step': 'file_selection', + 'status': 'failed', + 'message': str(e), + 'timestamp': datetime.now().isoformat() + } + workflow_steps.append(step_failed) + yield step_failed + + yield { + 'type': 'final_result', + 'success': False, + 'message': str(e), + 'workflow_steps': workflow_steps + } + return + + # 步骤3: 数据加载 + try: + step_data = { + 'type': 'workflow_step', + 'step': 'data_loading', + 'status': 'running', + 'message': '正在加载Excel数据...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + dataframes = await self._load_selected_dataframes(selected_files, user_id) + + step_completed = { + 'type': 'workflow_step', + 'step': 'data_loading', + 'status': 'completed', + 'message': f'成功加载{len(dataframes)}个数据表', + 'details': { + 'dataframe_count': len(dataframes), + 'total_rows': sum(len(df) for df in dataframes.values()) + }, + 'timestamp': datetime.now().isoformat() + } + workflow_steps.append(step_completed) + yield step_completed + logger.info(f"数据加载完成 - 共{len(dataframes)}个数据表") + + except Exception as e: + step_failed = { + 'type': 'workflow_step', + 'step': 'data_loading', + 'status': 'failed', + 'message': str(e), + 'timestamp': datetime.now().isoformat() + } + workflow_steps.append(step_failed) + yield step_failed + + yield { + 'type': 'final_result', + 'success': False, + 'message': str(e), + 'workflow_steps': workflow_steps + } + return + + # 步骤4: 代码执行 + try: + step_data = { + 'type': 'workflow_step', + 'step': 'code_execution', + 'status': 'running', + 'message': '正在生成并执行Python代码...', + 'timestamp': datetime.now().isoformat() + } + yield step_data + + result = await self._execute_smart_query(user_query, dataframes, selected_files) + + step_completed = { + 'type': 'workflow_step', + 'step': 'code_execution', + 'status': 'completed', + 'message': '成功执行Python代码分析', + 'details': { + 'result_type': result.get('result_type'), + 'data_count': result.get('total', 0) + }, + 'timestamp': datetime.now().isoformat() + } + workflow_steps.append(step_completed) + yield step_completed + logger.info("查询执行完成") + + # 发送最终结果 + yield { + 'type': 'final_result', + 'success': True, + 'data': result, + 'workflow_steps': workflow_steps + } + + except CodeExecutionError as e: + error_msg = f'代码执行失败: {str(e)}' + step_failed = { + 'type': 'workflow_step', + 'step': 'code_execution', + 'status': 'failed', + 'message': error_msg, + 'timestamp': datetime.now().isoformat() + } + workflow_steps.append(step_failed) + yield step_failed + logger.error(error_msg) + + yield { + 'type': 'final_result', + 'success': False, + 'message': error_msg, + 'workflow_steps': workflow_steps + } + return + + except SmartWorkflowError as e: + logger.error(f"智能工作流异常: {str(e)}") + yield { + 'type': 'final_result', + 'success': False, + 'message': str(e), + 'workflow_steps': workflow_steps + } + except Exception as e: + logger.error(f"智能工作流未知异常: {str(e)}", exc_info=True) + yield { + 'type': 'final_result', + 'success': False, + 'message': f'系统异常: {str(e)}', + 'workflow_steps': workflow_steps + } + + async def process_smart_query( + self, + user_query: str, + user_id: int, + conversation_id: Optional[int] = None, + is_new_conversation: bool = False + ) -> Dict[str, Any]: + """ + 处理智能问数查询的主要工作流 + + Args: + user_query: 用户问题 + user_id: 用户ID + conversation_id: 对话ID + is_new_conversation: 是否为新对话 + + Returns: + 包含查询结果的字典 + """ + workflow_steps = [] + + try: + logger.info(f"开始执行智能查询工作流 - 用户ID: {user_id}, 查询: {user_query[:50]}...") + + # 步骤1: 加载文件列表 + try: + if is_new_conversation or conversation_id is None: + file_list = await self._load_user_file_list(user_id) + if not file_list: + raise FileLoadError('未找到可用的Excel文件,请先上传文件') + else: + file_list = await self._load_user_file_list(user_id) + + workflow_steps.append({ + 'step': 'file_loading', + 'status': 'completed', + 'message': f'成功加载{len(file_list)}个文件', + 'details': {'file_count': len(file_list)} + }) + logger.info(f"文件加载完成 - 共{len(file_list)}个文件") + + except FileLoadError as e: + workflow_steps.append({ + 'step': 'file_loading', + 'status': 'failed', + 'message': str(e) + }) + return { + 'success': False, + 'message': str(e), + 'workflow_steps': workflow_steps + } + + # 步骤2: 智能文件选择 + try: + selected_files = await self._select_relevant_files(user_query, file_list) + + if not selected_files: + raise FileSelectionError('未找到与问题相关的Excel文件') + + workflow_steps.append({ + 'step': 'file_selection', + 'status': 'completed', + 'message': f'选择了{len(selected_files)}个相关文件', + 'selected_files': [f['filename'] for f in selected_files], + 'details': {'selection_count': len(selected_files)} + }) + logger.info(f"文件选择完成 - 选中{len(selected_files)}个文件") + + except FileSelectionError as e: + workflow_steps.append({ + 'step': 'file_selection', + 'status': 'failed', + 'message': str(e) + }) + return { + 'success': False, + 'message': str(e), + 'workflow_steps': workflow_steps + } + + # 步骤3: 加载DataFrame + try: + dataframes = await self._load_selected_dataframes(selected_files, user_id) + + if not dataframes: + raise FileLoadError('无法加载选中的Excel文件数据') + + workflow_steps.append({ + 'step': 'dataframe_loading', + 'status': 'completed', + 'message': f'成功加载{len(dataframes)}个数据表', + 'details': { + 'dataframe_count': len(dataframes), + 'total_rows': sum(len(df) for df in dataframes.values()) + } + }) + logger.info(f"DataFrame加载完成 - {len(dataframes)}个数据表") + + except Exception as e: + error_msg = f'数据加载失败: {str(e)}' + workflow_steps.append({ + 'step': 'dataframe_loading', + 'status': 'failed', + 'message': error_msg + }) + logger.error(error_msg) + return { + 'success': False, + 'message': error_msg, + 'workflow_steps': workflow_steps + } + + # 步骤4: 执行查询 + try: + result = await self._execute_smart_query(user_query, dataframes, selected_files) + + workflow_steps.append({ + 'step': 'code_execution', + 'status': 'completed', + 'message': '成功执行pandas代码分析', + 'details': { + 'result_type': result.get('result_type'), + 'data_count': result.get('total', 0) + } + }) + logger.info("查询执行完成") + + return { + 'success': True, + 'data': result, + 'workflow_steps': workflow_steps + } + + except CodeExecutionError as e: + error_msg = f'代码执行失败: {str(e)}' + workflow_steps.append({ + 'step': 'code_execution', + 'status': 'failed', + 'message': error_msg + }) + logger.error(error_msg) + return { + 'success': False, + 'message': error_msg, + 'workflow_steps': workflow_steps + } + + except SmartWorkflowError as e: + logger.error(f"智能工作流异常: {str(e)}") + return { + 'success': False, + 'message': str(e), + 'workflow_steps': workflow_steps + } + except Exception as e: + logger.error(f"工作流执行失败: {str(e)}", exc_info=True) + workflow_steps.append({ + 'step': 'error', + 'status': 'failed', + 'message': f'系统错误: {str(e)}' + }) + return { + 'success': False, + 'message': f'工作流执行失败: {str(e)}', + 'workflow_steps': workflow_steps + } + + async def _load_user_file_list(self, user_id: int) -> List[Dict[str, Any]]: + """ + 加载用户的所有文件列表信息 + """ + try: + # 从数据库获取用户的文件元数据 + file_metadata = [] + if self.metadata_service: + files, total = await self._run_in_executor( + self.metadata_service.get_user_files, user_id + ) + file_metadata = files + else: + logger.warning("metadata_service未初始化,跳过数据库文件查询") + + # 检查持久化目录中的文件 + persistent_dir = os.path.join("backend", "data", 'uploads', f"excel_{user_id}") + persistent_files = [] + if os.path.exists(persistent_dir): + persistent_files = [f for f in os.listdir(persistent_dir) + if f.endswith('.pkl')] + + file_list = [] + + # 合并数据库和持久化文件信息 + for metadata in file_metadata: + # 获取默认sheet的信息 + default_sheet = metadata.default_sheet or (metadata.sheet_names[0] if metadata.sheet_names else None) + columns = metadata.columns_info.get(default_sheet, []) if metadata.columns_info and default_sheet else [] + row_count = metadata.total_rows.get(default_sheet, 0) if metadata.total_rows and default_sheet else 0 + column_count = metadata.total_columns.get(default_sheet, 0) if metadata.total_columns and default_sheet else 0 + + file_info = { + 'id': metadata.id, + 'filename': metadata.original_filename, + 'file_path': metadata.file_path, + 'columns': columns, + 'row_count': row_count, + 'column_count': column_count, + 'description': f'Excel文件,包含{str(len(metadata.sheet_names))}个工作表' if metadata.sheet_names else '', + 'created_at': metadata.created_at.isoformat() if metadata.created_at else None + } + file_list.append(file_info) + + return file_list + + except Exception as e: + print(f"加载文件列表失败: {e}") + return [] + + async def _select_relevant_files( + self, + user_query: str, + file_list: List[Dict[str, Any]] + ) -> List[Dict[str, Any]]: + """ + 根据用户问题智能选择相关的Excel文件 + + Args: + user_query: 用户问题 + file_list: 可用文件列表 + + Returns: + 选中的文件列表 + + Raises: + FileSelectionError: 文件选择过程中发生错误 + """ + if not file_list: + logger.warning("文件列表为空,无法进行文件选择") + raise FileSelectionError("没有可用的文件进行选择") + + # 如果只有一个文件,直接返回 + if len(file_list) == 1: + logger.info("只有一个文件,直接选择") + return file_list + + try: + logger.info(f"开始智能文件选择 - 可用文件数: {len(file_list)}") + + # 构建文件选择提示 + file_descriptions = [] + for i, file_info in enumerate(file_list): + # 确保数值类型转换为字符串 + row_count = str(file_info.get('row_count', 'unknown')) + column_count = str(file_info.get('column_count', 'unknown')) + columns = file_info.get('columns', []) + # 确保列名都是字符串类型 + column_names = ', '.join(str(col) for col in columns) + + desc = f""" + 文件{i+1}: {file_info['filename']} + - 行数: {row_count} + - 列数: {column_count} + - 列名: {column_names} + - 描述: {file_info.get('description', '无描述')} + """ + file_descriptions.append(desc) + file_des_str = ' \n'.join(file_descriptions) + prompt = f""" + 用户问题: {user_query} + + 可用的Excel文件: + {file_des_str} + + 请分析用户问题,选择最相关的Excel文件来回答问题。 + 如果问题涉及多个文件的数据关联,可以选择多个文件。 + 如果问题只涉及特定类型的数据,只选择相关的文件。 + + 请返回JSON格式的结果,包含选中文件的索引(从1开始): + {{"selected_files": [1, 2, ...], "reason": "选择理由"}} + """ + + # 调用LLM进行文件选择 + response = await self._run_in_executor( + self.llm.invoke, [HumanMessage(content=prompt)] + ) + + # 解析LLM响应 + try: + import re + json_match = re.search(r'\{.*\}', response.content, re.DOTALL) + if json_match: + result = json.loads(json_match.group()) + selected_indices = result.get('selected_files', []) + reason = result.get('reason', '未提供理由') + + # 转换索引为实际文件 + selected_files = [] + for idx in selected_indices: + if 1 <= idx <= len(file_list): + selected_files.append(file_list[idx - 1]) + + if not selected_files: + logger.warning("LLM选择结果为空,回退到选择所有文件") + return file_list + + logger.info(f"成功选择{len(selected_files)}个文件: {[f['filename'] for f in selected_files]}") + logger.info(f"选择理由: {reason}") + return selected_files + else: + logger.warning("无法解析LLM响应中的JSON,回退到选择所有文件") + return file_list + + except (json.JSONDecodeError, KeyError) as e: + logger.warning(f"解析LLM响应失败: {str(e)},回退到选择所有文件") + return file_list + + except Exception as e: + raise e + logger.error(f"文件选择过程中发生错误: {str(e)}") + # 出错时返回所有文件作为备选方案 + logger.info("回退到选择所有文件") + return file_list + + async def _load_selected_dataframes( + self, + selected_files: List[Dict[str, Any]], + user_id: int + ) -> Dict[str, pd.DataFrame]: + """ + 加载选中的Excel文件为DataFrame + 使用新的持久化目录结构和文件匹配逻辑 + """ + dataframes = {} + + # 构建用户专属目录路径 + # base_dir = os.path.join("backend", "data", f"excel_{user_id}") + current_user_id = UserContext.get_current_user().id + backend_dir = Path(__file__).parent.parent.parent # 获取backend目录 + base_dir = backend_dir / "data/uploads" / f'excel_{current_user_id}' + if not os.path.exists(base_dir): + logger.warning(f"用户目录不存在: {base_dir}") + return dataframes + + try: + # 获取目录中所有文件 + all_files = os.listdir(base_dir) + + for file_info in selected_files: + filename = file_info.get('filename', '') + if not filename: + logger.warning(f"文件信息缺少filename: {file_info}") + continue + + # 查找匹配的文件(格式:{uuid}_{original_filename}) + matching_files = [] + for file in all_files: + if file.endswith(f"_{filename}") or file.endswith(f"_{filename}.pkl"): + matching_files.append(file) + + if not matching_files: + logger.warning(f"未找到匹配的文件: {filename}") + continue + + # 如果有多个匹配文件,选择最新的 + if len(matching_files) > 1: + matching_files.sort(key=lambda x: os.path.getmtime(os.path.join(base_dir, x)), reverse=True) + logger.info(f"找到多个匹配文件,选择最新的: {matching_files[0]}") + + selected_file = matching_files[0] + file_path = os.path.join(base_dir, selected_file) + + try: + # 优先加载pickle文件 + if selected_file.endswith('.pkl'): + df = await self._run_in_executor(pd.read_pickle, file_path) + logger.info(f"成功从pickle加载文件: {selected_file}") + else: + # 如果没有pickle文件,尝试加载原始文件 + if selected_file.endswith(('.xlsx', '.xls')): + df = await self._run_in_executor(pd.read_excel, file_path) + elif selected_file.endswith('.csv'): + df = await self._run_in_executor(pd.read_csv, file_path) + else: + logger.warning(f"不支持的文件格式: {selected_file}") + continue + logger.info(f"成功从原始文件加载: {selected_file}") + + # 使用原始文件名作为key + dataframes[filename] = df + logger.info(f"成功加载DataFrame: {filename}, 形状: {df.shape}") + + except Exception as e: + logger.error(f"加载文件失败 {selected_file}: {e}") + continue + + except Exception as e: + logger.error(f"加载DataFrames时发生错误: {e}") + raise FileLoadError(f"无法加载选中的文件: {e}") + + if not dataframes: + raise FileLoadError("没有成功加载任何文件") + + return dataframes + + def _parse_dataframe_string_to_table_data(self, df_string: str, subindex: int = -2) -> Dict[str, Any]: + """ + 将字符串格式的DataFrame转换为表格数据 + + Args: + df_string: DataFrame的字符串表示 + + Returns: + 包含columns和data的字典 + """ + try: + # 按行分割字符串 + lines = df_string.strip().split('\n') + + # 去掉最后两行(如因为最后两行可能是 "[12 rows x 11 columns]和空行") + + if len(lines) >= 2 and subindex == -2 : + lines = lines[:subindex] + + if len(lines) < 2: + # 如果行数不足,返回原始字符串 + return { + 'columns': [{'prop': 'result', 'label': '结果', 'width': 'auto'}], + 'data': [{'result': df_string}], + 'total': 1 + } + + # 第一行是列名 + header_line = lines[0].strip() + # 解析列名(去掉索引列) + columns_raw = header_line.split() + if columns_raw and columns_raw[0].isdigit() == False: + # 如果第一列不是数字,说明包含了列名 + column_names = columns_raw + else: + # 否则使用默认列名 + column_names = [f'Column_{i}' for i in range(len(columns_raw))] + + # 构建列定义 + columns = [] + for i, col_name in enumerate(column_names): + columns.append({ + 'prop': f'col_{i}', + 'label': str(col_name), + 'width': 'auto' + }) + + # 解析数据行 + data = [] + for line in lines[1:]: + if line.strip(): + # 分割数据行 + row_values = line.strip().split() + if row_values: + row_data = {} + # 第一个值通常是索引 + if len(row_values) > 0 and row_values[0].isdigit(): + row_data['_index'] = row_values[0] + values = row_values[1:] + else: + values = row_values + + # 填充列数据 + for i, value in enumerate(values): + if i < len(columns): + col_prop = f'col_{i}' + # 处理NaN值 + if value.lower() == 'nan': + row_data[col_prop] = '' + else: + row_data[col_prop] = value + + data.append(row_data) + + return { + 'columns': columns, + 'data': data, + 'total': len(data) + } + + except Exception as e: + logger.warning(f"解析DataFrame字符串失败: {str(e)}") + return { + 'columns': [{'prop': 'result', 'label': '结果', 'width': 'auto'}], + 'data': [{'result': df_string}], + 'total': 1 + } + + async def _execute_smart_query( + self, + user_query: str, + dataframes: Dict[str, pd.DataFrame], + selected_files: List[Dict[str, Any]] + ) -> Dict[str, Any]: + """ + 执行智能查询,生成和运行pandas代码 + + Args: + user_query: 用户查询 + dataframes: 加载的数据框字典 + selected_files: 选中的文件信息 + + Returns: + 查询结果字典 + + Raises: + CodeExecutionError: 代码执行失败 + """ + if not dataframes: + raise CodeExecutionError("没有可用的数据文件") + + logger.info(f"开始执行智能查询: {user_query[:50]}...") + + try: + # 如果有多个DataFrame,合并或选择主要的一个 + if len(dataframes) == 1: + main_df = list(dataframes.values())[0] + main_filename = list(dataframes.keys())[0] + else: + # 多个文件时,选择行数最多的作为主DataFrame + main_filename = max(dataframes.keys(), key=lambda k: len(dataframes[k])) + main_df = dataframes[main_filename] + + logger.info(f"选择主数据文件: {main_filename}, 行数: {len(main_df)}, 列数: {len(main_df.columns)}") + + # 验证数据框 + if main_df.empty: + raise CodeExecutionError(f"主数据文件 {main_filename} 为空") + + # 使用PythonAstREPLTool替代pandas代理 + try: + from langchain_experimental.tools import PythonAstREPLTool + from langchain_core.output_parsers import JsonOutputKeyToolsParser + from langchain_core.prompts import ChatPromptTemplate + + # 准备数据框字典,支持多个文件 + df_locals = {} + var_name_to_filename = {} # 变量名到文件名的映射 + for filename, df in dataframes.items(): + # 使用简化的变量名 + var_name = f"df_{len(df_locals) + 1}" if len(dataframes) > 1 else "df" + df_locals[var_name] = df + var_name_to_filename[var_name] = filename + + # 创建Python代码执行工具 + python_tool = PythonAstREPLTool(locals=df_locals) + + logger.info(f"创建Python工具成功,可用数据框: {list(df_locals.keys())}") + + except Exception as e: + raise CodeExecutionError(f"创建Python工具失败: {str(e)}") + + # 构建数据集信息(包含文件名和前5行数据) + dataset_info = [] + for var_name, df in df_locals.items(): + filename = var_name_to_filename[var_name] + + # 基本信息 + basic_info = f"- {var_name} (来源文件: {filename}): {len(df)}行 x {len(df.columns)}列" + + # 列名信息 + columns_info = f" 列名: {', '.join(str(col) for col in df.columns.tolist())}" + + # 前5行数据预览 + try: + preview_df = df.head(5) + preview_data = [] + for idx, row in preview_df.iterrows(): + row_data = [] + for col in df.columns: + value = row[col] + # 处理空值和特殊值 + if pd.isna(value): + row_data.append('NaN') + elif isinstance(value, (int, float)): + row_data.append(str(value)) + else: + # 限制字符串长度避免过长 + str_value = str(value) + if len(str_value) > 20: + str_value = str_value[:17] + '...' + row_data.append(str_value) + preview_data.append(f" 行{idx}: {', '.join(row_data)}") + + preview_info = f" 前5行数据预览:\n{chr(10).join(preview_data)}" + except Exception as e: + preview_info = f" 前5行数据预览: 无法生成预览 ({str(e)})" + + # 组合完整信息 + dataset_info.append(f"{basic_info}\n{columns_info}\n{preview_info}") + + # 构建系统提示 + system_prompt = f""" + 你所有可以访问的数据来自于传递给您的python_tool里的locals里的pandas数据信(可能有多个)。 + pandas数据集详细信息(文件来源、列名信息和数据预览)如下: + {chr(10).join(dataset_info)} + 请根据用户提出的问题,结合给出的数据集的详细信息,直接编写Python相关代码来计算pandas中的值。要求: + 1. 只返回代码,不返回其他内容 + 2. 只允许使用pandas和内置库 + 3. 确保代码能够直接执行并返回结果,包括import必要的内置库 + 4. 返回的结果应该是详细的、完整的数据,而不仅仅是简单答案 + 5. 结果应该包含足够的上下文信息,让用户能够验证和理解答案 + 6. 优先返回DataFrame格式的结果,便于展示为表格 + 7. 务必不要再去写代码查看数据集的结构,提示词里已经给出了每个数据集的结构信息,直接根据提示词里的结构信息进行判断。 + 8. 要求代码中最后一次print的结果,必需是最后的正确结果(用户所需要的数据) + + 示例: + - 如果问"哪个项目合同额最高",不仅要返回项目名称,还要返回跟该项目其他有用的信息,比如合同额,合同时间,项目类型等(如果表格有该这些字段信息) + - 如果问"销售额最高的产品",要返回产品名称、销售额、销售数量、市场占比等完整信息(如果表格有该这些字段信息) + """ + + # 创建提示模板 + prompt = ChatPromptTemplate([ + ("system", system_prompt), + ("user", "{question}") + ]) + + # 创建解析器 + parser = JsonOutputKeyToolsParser(key_name=python_tool.name, first_tool_only=True) + + # 绑定工具到LLM + llm_with_tools = self.llm.bind_tools([python_tool]) + + def debug_print(x): + print('中间结果:', x) + return x + + debug_node = RunnableLambda(debug_print) + # 创建执行链 + llm_chain = prompt | llm_with_tools | debug_node| parser | debug_node| python_tool| debug_node + + # 执行查询 + try: + logger.debug("开始执行Python工具查询") + result = await self._run_in_executor(llm_chain.invoke, {"question": user_query}) + logger.debug(f"查询执行完成,结果: {str(result)[:200]}...") + except Exception as e: + error_msg = f"Python工具执行失败: {str(e)}" + logger.error(error_msg) + raise CodeExecutionError(error_msg) + + # 处理结果 + + try: + # 检查结果是否为pandas DataFrame + print('result type:',type(result)) + parse_result = '' + if isinstance(result, pd.DataFrame): + # 转换为表格数据 + table_data = self._convert_dataframe_to_table_data(result) + + data = table_data['data'] + columns = table_data['columns'] + total = table_data['total'] + result_type = 'table_data' + logger.info(f"处理DataFrame结果: {len(result)}行 x {len(result.columns)}列") + parse_result = table_data + # PythonAstREPLTool返回的是字符串结果 + elif isinstance(result, str): + # 尝试解析结果中的数据 + result_lines = result.strip().split('\n') + + # 检查是否是DataFrame的字符串表示 + if any('DataFrame' in line or ('|' in line and len([l for l in result_lines if '|' in l]) > 1) for line in result_lines): + + table_data = self._parse_dataframe_string_to_table_data(result) + data = table_data['data'] + columns = table_data['columns'] + total = 1 + result_type = 'table_data' + parse_result = table_data + elif ('rows' in result_lines[-1] and 'columns' in result_lines[-1]): + # 尝试解析DataFrame字符串为表格数据 + table_data = self._parse_dataframe_string_to_table_data(result) + if 'data' in table_data and 'columns' in table_data: + data = table_data['data'] + columns = table_data['columns'] + total = 1 + result_type = 'table_data' + parse_result = table_data + else: + total = 1 + result_type = 'text' + parse_result = table_data + + else: + # 简单的数值或文本结果 + # 尝试解析DataFrame字符串为表格数据 + table_data = self._parse_dataframe_string_to_table_data(result, 0) + if 'data' in table_data and 'columns' in table_data: + data = table_data['data'] + columns = table_data['columns'] + total = table_data['total'] + total = 1 + result_type = 'table_data' + parse_result = table_data + else: + total = 1 + result_type = 'text' + parse_result = table_data + elif isinstance(result, (int, float, bool)): + data = result + columns = result + total = 1 + result_type = 'scalar' + parse_result = result + else: + # 处理其他类型的结果 + data = result + columns = result + total = 1 + result_type = 'other' + parse_result = result + logger.info(f"结果处理完成: {result_type}, 数据行数: {total}") + + except Exception as e: + error_msg = f"结果处理失败: {str(e)}" + logger.error(error_msg) + raise CodeExecutionError(error_msg) + + # 生成总结 + try: + summary = await self._generate_query_summary(user_query, parse_result, main_df) + except Exception as e: + logger.warning(f"生成总结失败: {str(e)}") + summary = f"基于数据分析完成查询,共处理{len(main_df)}行数据。" + + return { + 'data': data, + 'columns': columns, + 'total': total, + 'result_type': result_type, + 'summary': summary, + 'used_files': list(dataframes.keys()), + 'generated_code': f"# 基于文件: {', '.join(dataframes.keys())}\n# 查询: {user_query}\n# 使用LangChain Python工具执行", + 'data_info': { + 'source_files': list(dataframes.keys()), + 'dataframes': {name: {'rows': len(df), 'columns': len(df.columns), 'column_names': [str(col) for col in df.columns.tolist()]} for name, df in dataframes.items()} + } + } + + except CodeExecutionError: + raise + except Exception as e: + error_msg = f"查询执行过程中发生未知错误: {str(e)}" + logger.error(error_msg, exc_info=True) + raise CodeExecutionError(error_msg) + + async def _generate_query_summary( + self, + query: str, + result: Any, + df: pd.DataFrame + ) -> str: + """ + 生成查询结果的AI总结 + """ + try: + logger.debug("开始生成查询总结") + + # 安全地获取数据集信息 + try: + dataset_info = f""" + 数据集信息: + - 总行数: {len(df)} + - 总列数: {len(df.columns)} + - 列名: {', '.join(str(col) for col in df.columns.tolist())} + """ + except Exception as e: + logger.warning(f"获取数据集信息失败: {str(e)}") + dataset_info = "数据集信息: 无法获取" + + # 安全地处理查询结果 + try: + if isinstance(result, pd.DataFrame): + if len(result) > 0: + result_preview = result.head(3).to_string(max_cols=5, max_rows=3) + else: + result_preview = "查询结果为空" + else: + result_preview = str(result) # 限制长度避免过长 + except Exception as e: + logger.warning(f"生成结果预览失败: {str(e)}") + result_preview = "无法生成结果预览" + + prompt = f""" + 用户问题: {query} + + {dataset_info} + + 查询结果: {result_preview}... + + 系统已经根据用户提问查询出了结果,请根据结果生成一个简洁的中文总结,说明: + 1. 查询的主要发现 + 2. 数据的关键特征 + 3. 结果的业务含义 + + 总结应该在100字以内,通俗易懂。 + """ + + try: + response = await self._run_in_executor( + self.llm.invoke, [HumanMessage(content=prompt)] + ) + + summary = response.content.strip() + + # 验证总结长度 + if len(summary) > 200: + logger.warning("AI生成的总结过长,进行截取") + summary = summary[:200] + "..." + + logger.debug("查询总结生成完成") + return summary + + except Exception as e: + logger.warning(f"LLM总结生成失败: {str(e)}") + # 生成基础总结 + if isinstance(result, pd.DataFrame): + return f"基于{len(df)}行数据完成了关于'{query}'的分析,返回了{len(result)}条结果。" + else: + return f"基于{len(df)}行数据完成了关于'{query}'的分析查询。" + + except Exception as e: + logger.error(f"生成查询总结时发生错误: {str(e)}") + # 如果所有方法都失败,返回最基础的总结 + try: + return f"基于数据分析完成查询,共处理{len(df)}行数据。" + except: + return "完成了数据分析查询。" \ No newline at end of file diff --git a/backend/th_agenter/services/smart_query.py b/backend/th_agenter/services/smart_query.py new file mode 100644 index 0000000..5bda85f --- /dev/null +++ b/backend/th_agenter/services/smart_query.py @@ -0,0 +1,757 @@ +import pandas as pd +import tempfile + +# Try to import database modules +try: + import pymysql + PYMYSQL_AVAILABLE = True +except ImportError: + pymysql = None + PYMYSQL_AVAILABLE = False + +try: + import psycopg2 + PSYCOPG2_AVAILABLE = True +except ImportError: + psycopg2 = None + PSYCOPG2_AVAILABLE = False +import os +from typing import Dict, Any, List +from datetime import datetime +import asyncio +from concurrent.futures import ThreadPoolExecutor + +# Try to import langchain_experimental modules +try: + from langchain_experimental.agents import create_pandas_dataframe_agent + LANGCHAIN_EXPERIMENTAL_AVAILABLE = True +except ImportError: + create_pandas_dataframe_agent = None + LANGCHAIN_EXPERIMENTAL_AVAILABLE = False + +# Try to import langchain_community modules +try: + from langchain_community.chat_models import ChatZhipuAI + LANGCHAIN_COMMUNITY_AVAILABLE = True +except ImportError: + ChatZhipuAI = None + LANGCHAIN_COMMUNITY_AVAILABLE = False + +# Try to import langchain_core modules +try: + from langchain_core.messages import HumanMessage + LANGCHAIN_CORE_AVAILABLE = True +except ImportError: + HumanMessage = None + LANGCHAIN_CORE_AVAILABLE = False + +# 在 SmartQueryService 类中添加方法 + +from .table_metadata_service import TableMetadataService + +class SmartQueryService: + """ + 智能问数服务基类 + """ + def __init__(self): + self.executor = ThreadPoolExecutor(max_workers=4) + self.table_metadata_service = None + + def set_db_session(self, db_session): + """设置数据库会话""" + self.table_metadata_service = TableMetadataService(db_session) + + async def _run_in_executor(self, func, *args): + """在线程池中运行阻塞函数""" + loop = asyncio.get_event_loop() + return await loop.run_in_executor(self.executor, func, *args) + +class ExcelAnalysisService(SmartQueryService): + """ + Excel数据分析服务 + """ + def __init__(self): + super().__init__() + self.user_dataframes = {} # 存储用户的DataFrame + + def analyze_dataframe(self, df: pd.DataFrame, filename: str) -> Dict[str, Any]: + """ + 分析DataFrame并返回基本信息 + """ + try: + # 基本统计信息 + rows, columns = df.shape + + # 列信息 + column_info = [] + for col in df.columns: + col_info = { + 'name': col, + 'dtype': str(df[col].dtype), + 'null_count': int(df[col].isnull().sum()), + 'unique_count': int(df[col].nunique()) + } + + # 如果是数值列,添加统计信息 + if pd.api.types.is_numeric_dtype(df[col]): + df.fillna({col:0}) #数值列,将空值补0 + col_info.update({ + 'mean': float(df[col].mean()) if not df[col].isnull().all() else None, + 'std': float(df[col].std()) if not df[col].isnull().all() else None, + 'min': float(df[col].min()) if not df[col].isnull().all() else None, + 'max': float(df[col].max()) if not df[col].isnull().all() else None + }) + + column_info.append(col_info) + + # 数据预览(前5行) + preview_data = df.head().fillna('').to_dict('records') + + # 数据质量检查 + quality_issues = [] + + # 检查缺失值 + missing_cols = df.columns[df.isnull().any()].tolist() + if missing_cols: + quality_issues.append({ + 'type': 'missing_values', + 'description': f'以下列存在缺失值: {", ".join(map(str, missing_cols))}', + 'columns': missing_cols + }) + + # 检查重复行 + duplicate_count = df.duplicated().sum() + if duplicate_count > 0: + quality_issues.append({ + 'type': 'duplicate_rows', + 'description': f'发现 {duplicate_count} 行重复数据', + 'count': int(duplicate_count) + }) + + return { + 'filename': filename, + 'rows': rows, + 'columns': columns, + 'column_names': [str(col) for col in df.columns.tolist()], + 'column_info': column_info, + 'preview': preview_data, + 'quality_issues': quality_issues, + 'memory_usage': f"{df.memory_usage(deep=True).sum() / 1024 / 1024:.2f} MB" + } + + except Exception as e: + print(e) + raise Exception(f"DataFrame分析失败: {str(e)}") + + def _create_pandas_agent(self, df: pd.DataFrame): + """ + 创建pandas代理 + """ + try: + if not LANGCHAIN_COMMUNITY_AVAILABLE: + raise Exception("langchain_community not available. Cannot create pandas agent.") + if not LANGCHAIN_EXPERIMENTAL_AVAILABLE: + raise Exception("langchain_experimental not available. Cannot create pandas agent.") + + # 使用智谱AI作为LLM + llm = ChatZhipuAI( + model="glm-4", + api_key=os.getenv("ZHIPUAI_API_KEY"), + temperature=0.1 + ) + + # 创建pandas代理 + agent = create_pandas_dataframe_agent( + llm=llm, + df=df, + verbose=True, + return_intermediate_steps=True, + handle_parsing_errors=True, + max_iterations=3, + early_stopping_method="force", + allow_dangerous_code=True # 允许执行代码以支持数据分析 + ) + + return agent + + except Exception as e: + raise Exception(f"创建pandas代理失败: {str(e)}") + + def _execute_pandas_query(self, agent, query: str) -> Dict[str, Any]: + """ + 执行pandas查询 + """ + try: + # 执行查询 + # 使用invoke方法来处理有多个输出键的情况 + agent_result = agent.invoke({"input": query}) + # 提取主要结果 + result = agent_result.get('output', agent_result) + + # 解析结果 + if isinstance(result, pd.DataFrame): + # 如果结果是DataFrame + data = result.fillna('').to_dict('records') + columns = result.columns.tolist() + total = len(result) + + return { + 'data': data, + 'columns': columns, + 'total': total, + 'result_type': 'dataframe' + } + else: + # 如果结果是其他类型(字符串、数字等) + return { + 'data': [{'result': str(result)}], + 'columns': ['result'], + 'total': 1, + 'result_type': 'scalar' + } + + except Exception as e: + raise Exception(f"pandas查询执行失败: {str(e)}") + + async def execute_natural_language_query( + self, + query: str, + user_id: int, + page: int = 1, + page_size: int = 20 + ) -> Dict[str, Any]: + """ + 执行自然语言查询 + """ + try: + # 查找用户的临时文件 + temp_dir = tempfile.gettempdir() + user_files = [f for f in os.listdir(temp_dir) + if f.startswith(f"excel_{user_id}_") and f.endswith('.pkl')] + + if not user_files: + return { + 'success': False, + 'message': '未找到上传的Excel文件,请先上传文件' + } + + # 使用最新的文件 + latest_file = sorted(user_files)[-1] + file_path = os.path.join(temp_dir, latest_file) + + # 加载DataFrame + df = pd.read_pickle(file_path) + + # 创建pandas代理 + agent = self._create_pandas_agent(df) + + # 执行查询 + query_result = await self._run_in_executor( + self._execute_pandas_query, agent, query + ) + + # 分页处理 + total = query_result['total'] + start_idx = (page - 1) * page_size + end_idx = start_idx + page_size + + paginated_data = query_result['data'][start_idx:end_idx] + + # 生成AI总结 + summary = await self._generate_summary(query, query_result, df) + + return { + 'success': True, + 'data': { + 'data': paginated_data, + 'columns': query_result['columns'], + 'total': total, + 'page': page, + 'page_size': page_size, + 'generated_code': f"# 基于自然语言查询: {query}\n# 使用LangChain Pandas代理执行", + 'summary': summary, + 'result_type': query_result['result_type'] + } + } + + except Exception as e: + return { + 'success': False, + 'message': f"查询执行失败: {str(e)}" + } + + async def _generate_summary(self, query: str, result: Dict[str, Any], df: pd.DataFrame) -> str: + """ + 生成AI总结 + """ + try: + llm = ChatZhipuAI( + model="glm-4", + api_key=os.getenv("ZHIPUAI_API_KEY"), + temperature=0.3 + ) + + # 构建总结提示 + prompt = f""" + 用户查询: {query} + + 数据集信息: + - 总行数: {len(df)} + - 总列数: {len(df.columns)} + - 列名: {', '.join(str(col) for col in df.columns.tolist())} + + 查询结果: + - 结果类型: {result['result_type']} + - 结果行数: {result['total']} + - 结果列数: {len(result['columns'])} + + 请基于以上信息,用中文生成一个简洁的分析总结,包括: + 1. 查询的主要目的 + 2. 关键发现 + 3. 数据洞察 + 4. 建议的后续分析方向 + + 总结应该专业、准确、易懂,控制在200字以内。 + """ + + response = await self._run_in_executor( + lambda: llm.invoke([HumanMessage(content=prompt)]) + ) + + return response.content + + except Exception as e: + return f"查询已完成,但生成总结时出现错误: {str(e)}" + +class DatabaseQueryService(SmartQueryService): + """ + 数据库查询服务 + """ + def __init__(self): + super().__init__() + self.user_connections = {} # 存储用户的数据库连接信息 + + def _create_connection(self, config: Dict[str, str]): + """ + 创建数据库连接 + """ + db_type = config['type'].lower() + + try: + if db_type == 'mysql': + if not PYMYSQL_AVAILABLE: + raise Exception("MySQL数据库连接功能不可用,请安装pymysql模块") + connection = pymysql.connect( + host=config['host'], + port=int(config['port']), + user=config['username'], + password=config['password'], + database=config['database'], + charset='utf8mb4' + ) + elif db_type == 'postgresql': + if not PSYCOPG2_AVAILABLE: + raise Exception("PostgreSQL数据库连接功能不可用,请安装psycopg2模块") + connection = psycopg2.connect( + host=config['host'], + port=int(config['port']), + user=config['username'], + password=config['password'], + database=config['database'] + ) + + return connection + + except Exception as e: + raise Exception(f"数据库连接失败: {str(e)}") + + async def test_connection(self, config: Dict[str, str]) -> bool: + """ + 测试数据库连接 + """ + try: + connection = await self._run_in_executor(self._create_connection, config) + connection.close() + return True + except Exception: + return False + + async def connect_database(self, config: Dict[str, str], user_id: int) -> Dict[str, Any]: + """ + 连接数据库并获取表列表 + """ + try: + connection = await self._run_in_executor(self._create_connection, config) + + # 获取表列表 + tables = await self._run_in_executor(self._get_tables, connection, config['type']) + + # 存储连接信息 + self.user_connections[user_id] = { + 'config': config, + 'connection': connection, + 'connected_at': datetime.now() + } + + return { + 'success': True, + 'data': { + 'tables': tables, + 'database_type': config['type'], + 'database_name': config['database'] + } + } + + except Exception as e: + return { + 'success': False, + 'message': f"数据库连接失败: {str(e)}" + } + + def _get_tables(self, connection, db_type: str) -> List[str]: + """ + 获取数据库表列表 + """ + cursor = connection.cursor() + + try: + if db_type.lower() == 'mysql': + cursor.execute("SHOW TABLES") + tables = [row[0] for row in cursor.fetchall()] + elif db_type.lower() == 'postgresql': + cursor.execute(""" + SELECT table_name + FROM information_schema.tables + WHERE table_schema = 'public' + """) + tables = [row[0] for row in cursor.fetchall()] + + elif db_type.lower() == 'sqlserver': + cursor.execute(""" + SELECT table_name + FROM information_schema.tables + WHERE table_type = 'BASE TABLE' + """) + tables = [row[0] for row in cursor.fetchall()] + else: + tables = [] + + return tables + + finally: + cursor.close() + + async def get_table_schema(self, table_name: str, user_id: int) -> Dict[str, Any]: + """ + 获取表结构 + """ + try: + if user_id not in self.user_connections: + return { + 'success': False, + 'message': '数据库连接已断开,请重新连接' + } + + connection = self.user_connections[user_id]['connection'] + db_type = self.user_connections[user_id]['config']['type'] + + schema = await self._run_in_executor( + self._get_table_schema, connection, table_name, db_type + ) + + return { + 'success': True, + 'data': { + 'schema': schema, + 'table_name': table_name + } + } + + except Exception as e: + return { + 'success': False, + 'message': f"获取表结构失败: {str(e)}" + } + + def _get_table_schema(self, connection, table_name: str, db_type: str) -> List[Dict[str, Any]]: + """ + 获取表结构信息 + """ + cursor = connection.cursor() + + try: + if db_type.lower() == 'mysql': + cursor.execute(f"DESCRIBE {table_name}") + columns = cursor.fetchall() + schema = [{ + 'column_name': col[0], + 'data_type': col[1], + 'is_nullable': 'YES' if col[2] == 'YES' else 'NO', + 'column_key': col[3], + 'column_default': col[4] + } for col in columns] + elif db_type.lower() == 'postgresql': + cursor.execute(""" + SELECT column_name, data_type, is_nullable, column_default + FROM information_schema.columns + WHERE table_name = %s + ORDER BY ordinal_position + """, (table_name,)) + columns = cursor.fetchall() + schema = [{ + 'column_name': col[0], + 'data_type': col[1], + 'is_nullable': col[2], + 'column_default': col[3] + } for col in columns] + + else: + schema = [] + + return schema + + finally: + cursor.close() + + async def execute_natural_language_query( + self, + query: str, + table_name: str, + user_id: int, + page: int = 1, + page_size: int = 20 + ) -> Dict[str, Any]: + """ + 执行自然语言数据库查询 + """ + try: + if user_id not in self.user_connections: + return { + 'success': False, + 'message': '数据库连接已断开,请重新连接' + } + + connection = self.user_connections[user_id]['connection'] + + # 这里应该集成MCP服务来将自然语言转换为SQL + # 目前先使用简单的实现 + sql_query = await self._convert_to_sql(query, table_name, connection) + + # 执行SQL查询 + result = await self._run_in_executor( + self._execute_sql_query, connection, sql_query, page, page_size + ) + + # 生成AI总结 + summary = await self._generate_db_summary(query, result, table_name) + + result['generated_code'] = sql_query + result['summary'] = summary + + return { + 'success': True, + 'data': result + } + + except Exception as e: + return { + 'success': False, + 'message': f"数据库查询执行失败: {str(e)}" + } + + async def _convert_to_sql(self, query: str, table_name: str, connection) -> str: + """ + 将自然语言转换为SQL查询 + TODO: 集成MCP服务 + """ + # 这是一个简化的实现,实际应该使用MCP服务 + # 根据常见的查询模式生成SQL + + query_lower = query.lower() + + if '所有' in query or '全部' in query or 'all' in query_lower: + return f"SELECT * FROM {table_name} LIMIT 100" + elif '统计' in query or '总数' in query or 'count' in query_lower: + return f"SELECT COUNT(*) as total_count FROM {table_name}" + elif '最近' in query or 'recent' in query_lower: + return f"SELECT * FROM {table_name} ORDER BY id DESC LIMIT 10" + elif '分组' in query or 'group' in query_lower: + # 简单的分组查询,需要根据实际表结构调整 + return f"SELECT COUNT(*) as count FROM {table_name} GROUP BY id LIMIT 10" + else: + # 默认查询 + return f"SELECT * FROM {table_name} LIMIT 20" + + def _execute_sql_query(self, connection, sql_query: str, page: int, page_size: int) -> Dict[str, Any]: + """ + 执行SQL查询 + """ + cursor = connection.cursor() + + try: + # 执行查询 + cursor.execute(sql_query) + + # 获取列名 + columns = [desc[0] for desc in cursor.description] if cursor.description else [] + + # 获取所有结果 + all_results = cursor.fetchall() + total = len(all_results) + + # 分页 + start_idx = (page - 1) * page_size + end_idx = start_idx + page_size + paginated_results = all_results[start_idx:end_idx] + + # 转换为字典格式 + data = [] + for row in paginated_results: + row_dict = {} + for i, value in enumerate(row): + if i < len(columns): + row_dict[columns[i]] = value + data.append(row_dict) + + return { + 'data': data, + 'columns': columns, + 'total': total, + 'page': page, + 'page_size': page_size + } + + finally: + cursor.close() + + async def _generate_db_summary(self, query: str, result: Dict[str, Any], table_name: str) -> str: + """ + 生成数据库查询总结 + """ + try: + llm = ChatZhipuAI( + model="glm-4", + api_key=os.getenv("ZHIPUAI_API_KEY"), + temperature=0.3 + ) + + prompt = f""" + 用户查询: {query} + 目标表: {table_name} + + 查询结果: + - 结果行数: {result['total']} + - 结果列数: {len(result['columns'])} + - 列名: {', '.join(result['columns'])} + + 请基于以上信息,用中文生成一个简洁的数据库查询分析总结,包括: + 1. 查询的主要目的 + 2. 关键数据发现 + 3. 数据特征分析 + 4. 建议的后续查询方向 + + 总结应该专业、准确、易懂,控制在200字以内。 + """ + + response = await self._run_in_executor( + lambda: llm.invoke([HumanMessage(content=prompt)]) + ) + + return response.content + + except Exception as e: + return f"查询已完成,但生成总结时出现错误: {str(e)}" + + # 在 SmartQueryService 类中添加方法 + + from .table_metadata_service import TableMetadataService + + class SmartQueryService: + def __init__(self): + super().__init__() + self.table_metadata_service = None + + def set_db_session(self, db_session): + """设置数据库会话""" + self.table_metadata_service = TableMetadataService(db_session) + + async def get_database_context(self, user_id: int, query: str) -> str: + """获取数据库上下文信息用于问答""" + if not self.table_metadata_service: + return "" + + try: + # 获取用户的表元数据 + table_metadata_list = self.table_metadata_service.get_user_table_metadata(user_id) + + if not table_metadata_list: + return "" + + # 构建数据库上下文 + context_parts = [] + context_parts.append("=== 数据库表信息 ===") + + for metadata in table_metadata_list: + table_info = [] + table_info.append(f"表名: {metadata.table_name}") + + if metadata.table_comment: + table_info.append(f"表描述: {metadata.table_comment}") + + if metadata.qa_description: + table_info.append(f"业务说明: {metadata.qa_description}") + + # 添加列信息 + if metadata.columns_info: + columns = [] + for col in metadata.columns_info: + col_desc = f"{col['column_name']} ({col['data_type']})" + if col.get('column_comment'): + col_desc += f" - {col['column_comment']}" + columns.append(col_desc) + table_info.append(f"字段: {', '.join(columns)}") + + # 添加示例数据 + if metadata.sample_data: + table_info.append(f"示例数据: {metadata.sample_data[:2]}") + + table_info.append(f"总行数: {metadata.row_count}") + + context_parts.append("\n".join(table_info)) + context_parts.append("---") + + return "\n".join(context_parts) + + except Exception as e: + logger.error(f"获取数据库上下文失败: {str(e)}") + return "" + + async def execute_smart_query(self, query: str, user_id: int, **kwargs) -> Dict[str, Any]: + """执行智能查询(集成表元数据)""" + try: + # 获取数据库上下文 + db_context = await self.get_database_context(user_id, query) + + # 构建增强的提示词 + enhanced_prompt = f""" + {db_context} + + 用户问题: {query} + + 请基于上述数据库表信息,生成相应的SQL查询语句。 + 注意: + 1. 使用准确的表名和字段名 + 2. 考虑数据类型和约束 + 3. 参考示例数据理解数据格式 + 4. 生成高效的查询语句 + """ + + # 调用原有的查询逻辑 + return await super().execute_smart_query(enhanced_prompt, user_id, **kwargs) + + except Exception as e: + logger.error(f"智能查询失败: {str(e)}") + return { + 'success': False, + 'message': f"查询失败: {str(e)}" + } \ No newline at end of file diff --git a/backend/th_agenter/services/smart_workflow.py b/backend/th_agenter/services/smart_workflow.py new file mode 100644 index 0000000..fce1d9b --- /dev/null +++ b/backend/th_agenter/services/smart_workflow.py @@ -0,0 +1,83 @@ +from typing import Dict, Any, List, Optional, Union +import logging +from .smart_excel_workflow import SmartExcelWorkflowManager +from .smart_db_workflow import SmartDatabaseWorkflowManager + +logger = logging.getLogger(__name__) + +# 异常类已迁移到各自的工作流文件中 + +class SmartWorkflowManager: + """ + 智能工作流管理器 + 统一入口,委托给具体的Excel或数据库工作流管理器 + """ + + def __init__(self, db=None): + self.db = db + self.excel_workflow = SmartExcelWorkflowManager(db) + self.database_workflow = SmartDatabaseWorkflowManager(db) + + async def process_excel_query_stream( + self, + user_query: str, + user_id: int, + conversation_id: Optional[int] = None, + is_new_conversation: bool = False + ): + """ + 流式处理Excel智能问数查询,委托给Excel工作流管理器 + """ + async for result in self.excel_workflow.process_excel_query_stream( + user_query, user_id, conversation_id, is_new_conversation + ): + yield result + + async def process_database_query_stream( + self, + user_query: str, + user_id: int, + database_config_id: int, + conversation_id: Optional[int] = None, + is_new_conversation: bool = False + ): + """ + 流式处理数据库智能问数查询,委托给数据库工作流管理器 + """ + async for result in self.database_workflow.process_database_query_stream( + user_query, user_id, database_config_id + ): + yield result + + async def process_smart_query( + self, + user_query: str, + user_id: int, + conversation_id: Optional[int] = None, + is_new_conversation: bool = False + ) -> Dict[str, Any]: + """ + 处理智能问数查询的主要工作流(非流式版本) + 委托给Excel工作流管理器 + """ + return await self.excel_workflow.process_smart_query( + user_query=user_query, + user_id=user_id, + conversation_id=conversation_id, + is_new_conversation=is_new_conversation + ) + + async def process_database_query( + self, + user_query: str, + user_id: int, + database_config_id: int, + conversation_id: Optional[int] = None, + is_new_conversation: bool = False + ) -> Dict[str, Any]: + """ + 处理数据库智能问数查询,委托给数据库工作流管理器 + """ + return await self.database_workflow.process_database_query( + user_query, user_id, database_config_id, None, conversation_id, is_new_conversation + ) \ No newline at end of file diff --git a/backend/th_agenter/services/storage.py b/backend/th_agenter/services/storage.py new file mode 100644 index 0000000..b1cb6db --- /dev/null +++ b/backend/th_agenter/services/storage.py @@ -0,0 +1,299 @@ +"""File storage service supporting local and S3 storage.""" + +import os +import uuid +from abc import ABC, abstractmethod +from pathlib import Path +from typing import Optional, BinaryIO, Dict, Any +from fastapi import UploadFile + +# Try to import boto3 and related modules for S3 storage +try: + import boto3 + from botocore.exceptions import ClientError, NoCredentialsError + S3_AVAILABLE = True +except ImportError: + boto3 = None + ClientError = None + NoCredentialsError = None + S3_AVAILABLE = False + +from ..core.config import settings +from ..utils.file_utils import FileUtils + + +class StorageBackend(ABC): + """Abstract storage backend interface.""" + + @abstractmethod + async def upload_file( + self, + file: UploadFile, + file_path: str + ) -> Dict[str, Any]: + """Upload file and return storage info.""" + pass + + @abstractmethod + async def delete_file(self, file_path: str) -> bool: + """Delete file from storage.""" + pass + + @abstractmethod + async def get_file_url(self, file_path: str) -> Optional[str]: + """Get file access URL.""" + pass + + @abstractmethod + async def file_exists(self, file_path: str) -> bool: + """Check if file exists.""" + pass + + +class LocalStorageBackend(StorageBackend): + """Local file system storage backend.""" + + def __init__(self, base_path: str): + self.base_path = Path(base_path) + self.base_path.mkdir(parents=True, exist_ok=True) + + async def upload_file( + self, + file: UploadFile, + file_path: str + ) -> Dict[str, Any]: + """Upload file to local storage.""" + full_path = self.base_path / file_path + + # Create directory if it doesn't exist + full_path.parent.mkdir(parents=True, exist_ok=True) + + # Write file + with open(full_path, "wb") as f: + content = await file.read() + f.write(content) + + # Get file info + file_info = FileUtils.get_file_info(str(full_path)) + + return { + "file_path": file_path, + "full_path": str(full_path), + "size": file_info["size_bytes"], + "mime_type": file_info["mime_type"], + "storage_type": "local" + } + + async def delete_file(self, file_path: str) -> bool: + """Delete file from local storage.""" + full_path = self.base_path / file_path + return FileUtils.delete_file(str(full_path)) + + async def get_file_url(self, file_path: str) -> Optional[str]: + """Get local file URL (for development).""" + # In production, you might want to serve files through a web server + full_path = self.base_path / file_path + if full_path.exists(): + return f"/files/{file_path}" + return None + + async def file_exists(self, file_path: str) -> bool: + """Check if file exists in local storage.""" + full_path = self.base_path / file_path + return full_path.exists() + + +if S3_AVAILABLE: + class S3StorageBackend(StorageBackend): + """Amazon S3 storage backend.""" + + def __init__( + self, + bucket_name: str, + aws_access_key_id: Optional[str] = None, + aws_secret_access_key: Optional[str] = None, + aws_region: str = "us-east-1", + endpoint_url: Optional[str] = None + ): + self.bucket_name = bucket_name + self.aws_region = aws_region + + # Initialize S3 client + session = boto3.Session( + aws_access_key_id=aws_access_key_id, + aws_secret_access_key=aws_secret_access_key, + region_name=aws_region + ) + + self.s3_client = session.client( + 's3', + endpoint_url=endpoint_url # For S3-compatible services like MinIO + ) + + # Verify bucket exists or create it + self._ensure_bucket_exists() + + def _ensure_bucket_exists(self): + """Ensure S3 bucket exists.""" + try: + self.s3_client.head_bucket(Bucket=self.bucket_name) + except ClientError as e: + error_code = int(e.response['Error']['Code']) + if error_code == 404: + # Bucket doesn't exist, create it + try: + if self.aws_region == 'us-east-1': + self.s3_client.create_bucket(Bucket=self.bucket_name) + else: + self.s3_client.create_bucket( + Bucket=self.bucket_name, + CreateBucketConfiguration={'LocationConstraint': self.aws_region} + ) + except ClientError as create_error: + raise Exception(f"Failed to create S3 bucket: {create_error}") + else: + raise Exception(f"Failed to access S3 bucket: {e}") + + async def upload_file( + self, + file: UploadFile, + file_path: str + ) -> Dict[str, Any]: + """Upload file to S3.""" + try: + # Read file content + content = await file.read() + + # Determine content type + content_type = FileUtils.get_mime_type(file.filename) or 'application/octet-stream' + + # Upload to S3 + self.s3_client.put_object( + Bucket=self.bucket_name, + Key=file_path, + Body=content, + ContentType=content_type, + Metadata={ + 'original_filename': file.filename or 'unknown', + 'upload_timestamp': str(int(os.time.time())) + } + ) + + return { + "file_path": file_path, + "bucket": self.bucket_name, + "size": len(content), + "mime_type": content_type, + "storage_type": "s3" + } + except (ClientError, NoCredentialsError) as e: + raise Exception(f"Failed to upload file to S3: {e}") + + async def delete_file(self, file_path: str) -> bool: + """Delete file from S3.""" + try: + self.s3_client.delete_object( + Bucket=self.bucket_name, + Key=file_path + ) + return True + except ClientError: + return False + + async def get_file_url(self, file_path: str) -> Optional[str]: + """Get presigned URL for S3 file.""" + try: + url = self.s3_client.generate_presigned_url( + 'get_object', + Params={'Bucket': self.bucket_name, 'Key': file_path}, + ExpiresIn=3600 # 1 hour + ) + return url + except ClientError: + return None + + async def file_exists(self, file_path: str) -> bool: + """Check if file exists in S3.""" + try: + self.s3_client.head_object( + Bucket=self.bucket_name, + Key=file_path + ) + return True + except ClientError: + return False + + +class StorageService: + """统一的存储服务管理器""" + + def __init__(self): + self.storage_type = settings.storage.storage_type + + if self.storage_type == 's3': + if S3_AVAILABLE: + self.backend = S3StorageBackend( + bucket_name=settings.storage.s3_bucket_name, + aws_access_key_id=settings.storage.aws_access_key_id, + aws_secret_access_key=settings.storage.aws_secret_access_key, + aws_region=settings.storage.aws_region, + endpoint_url=settings.storage.s3_endpoint_url + ) + else: + # Fallback to local storage if S3 is not available + from ..utils.logger import get_logger + logger = get_logger("storage_service") + logger.warning("S3 storage not available. Falling back to local storage.") + # 确保使用绝对路径,避免在不同目录运行时路径不一致 + upload_dir = settings.storage.upload_directory + if not os.path.isabs(upload_dir): + # 如果是相对路径,则基于项目根目录计算绝对路径 + # 项目根目录是backend的父目录 + backend_dir = Path(__file__).parent.parent.parent + upload_dir = str(backend_dir / upload_dir) + self.backend = LocalStorageBackend(upload_dir) + else: + # 确保使用绝对路径,避免在不同目录运行时路径不一致 + upload_dir = settings.storage.upload_directory + if not os.path.isabs(upload_dir): + # 如果是相对路径,则基于项目根目录计算绝对路径 + # 项目根目录是backend的父目录 + backend_dir = Path(__file__).parent.parent.parent + upload_dir = str(backend_dir / upload_dir) + self.backend = LocalStorageBackend(upload_dir) + + def generate_file_path(self, knowledge_base_id: int, filename: str) -> str: + """Generate unique file path for storage.""" + # Sanitize filename + safe_filename = FileUtils.sanitize_filename(filename) + + # Generate unique identifier + file_id = str(uuid.uuid4()) + + # Create path: kb_{id}/{file_id}_{filename} + return f"kb_{knowledge_base_id}/{file_id}_{safe_filename}" + + async def upload_file( + self, + file: UploadFile, + knowledge_base_id: int + ) -> Dict[str, Any]: + """Upload file using configured storage backend.""" + file_path = self.generate_file_path(knowledge_base_id, file.filename) + return await self.backend.upload_file(file, file_path) + + async def delete_file(self, file_path: str) -> bool: + """Delete file using configured storage backend.""" + return await self.backend.delete_file(file_path) + + async def get_file_url(self, file_path: str) -> Optional[str]: + """Get file access URL.""" + return await self.backend.get_file_url(file_path) + + async def file_exists(self, file_path: str) -> bool: + """Check if file exists.""" + return await self.backend.file_exists(file_path) + + +# Global storage service instance +storage_service = StorageService() \ No newline at end of file diff --git a/backend/th_agenter/services/table_metadata_service.py b/backend/th_agenter/services/table_metadata_service.py new file mode 100644 index 0000000..c34fab8 --- /dev/null +++ b/backend/th_agenter/services/table_metadata_service.py @@ -0,0 +1,446 @@ +"""表元数据管理服务""" + +import json +from typing import List, Dict, Any, Optional +from sqlalchemy.orm import Session +from sqlalchemy import and_, func +from datetime import datetime + +from ..models.table_metadata import TableMetadata +from ..models.database_config import DatabaseConfig +from ..utils.logger import get_logger +from ..utils.exceptions import ValidationError, NotFoundError +from .postgresql_tool_manager import get_postgresql_tool +from .mysql_tool_manager import get_mysql_tool + +logger = get_logger("table_metadata_service") + + +class TableMetadataService: + """表元数据管理服务""" + + def __init__(self, db_session: Session): + self.db = db_session + self.postgresql_tool = get_postgresql_tool() + self.mysql_tool = get_mysql_tool() + + async def collect_and_save_table_metadata( + self, + user_id: int, + database_config_id: int, + table_names: List[str] + ) -> Dict[str, Any]: + """收集并保存表元数据""" + try: + # 获取数据库配置 + db_config = self.db.query(DatabaseConfig).filter( + and_(DatabaseConfig.id == database_config_id, DatabaseConfig.created_by == user_id) + ).first() + + if not db_config: + raise NotFoundError("数据库配置不存在") + + # 根据数据库类型选择相应的工具 + if db_config.db_type.lower() == 'postgresql': + db_tool = self.postgresql_tool + elif db_config.db_type.lower() == 'mysql': + db_tool = self.mysql_tool + else: + raise Exception(f"不支持的数据库类型: {db_config.db_type}") + + # 检查是否已有连接,如果没有则建立连接 + user_id_str = str(user_id) + if user_id_str not in db_tool.connections: + connection_config = { + 'host': db_config.host, + 'port': db_config.port, + 'database': db_config.database, + 'username': db_config.username, + 'password': self._decrypt_password(db_config.password) + } + + # 连接数据库 + connect_result = await db_tool.execute( + operation="connect", + connection_config=connection_config, + user_id=user_id_str + ) + + if not connect_result.success: + raise Exception(f"数据库连接失败: {connect_result.error}") + + logger.info(f"为用户 {user_id} 建立了新的{db_config.db_type}数据库连接") + else: + logger.info(f"复用用户 {user_id} 的现有{db_config.db_type}数据库连接") + + collected_tables = [] + failed_tables = [] + + for table_name in table_names: + try: + # 收集表元数据 + metadata = await self._collect_single_table_metadata( + user_id, table_name, db_config.db_type + ) + + # 保存或更新元数据 + table_metadata = await self._save_table_metadata( + user_id, database_config_id, table_name, metadata + ) + + collected_tables.append({ + 'table_name': table_name, + 'metadata_id': table_metadata.id, + 'columns_count': len(metadata['columns_info']), + 'sample_rows': len(metadata['sample_data']) + }) + + except Exception as e: + print(e) + logger.error(f"收集表 {table_name} 元数据失败: {str(e)}") + failed_tables.append({ + 'table_name': table_name, + 'error': str(e) + }) + + return { + 'success': True, + 'collected_tables': collected_tables, + 'failed_tables': failed_tables, + 'total_collected': len(collected_tables), + 'total_failed': len(failed_tables) + } + + except Exception as e: + logger.error(f"收集表元数据失败: {str(e)}") + return { + 'success': False, + 'message': str(e) + } + + async def _collect_single_table_metadata( + self, + user_id: int, + table_name: str, + db_type: str + ) -> Dict[str, Any]: + """收集单个表的元数据""" + + # 根据数据库类型选择相应的工具 + if db_type.lower() == 'postgresql': + db_tool = self.postgresql_tool + elif db_type.lower() == 'mysql': + db_tool = self.mysql_tool + else: + raise Exception(f"不支持的数据库类型: {db_type}") + + # 获取表结构 + schema_result = await db_tool.execute( + operation="describe_table", + user_id=str(user_id), + table_name=table_name + ) + + if not schema_result.success: + raise Exception(f"获取表结构失败: {schema_result.error}") + + schema_data = schema_result.result + + # 获取示例数据(前5条) + sample_result = await db_tool.execute( + operation="execute_query", + user_id=str(user_id), + sql_query=f"SELECT * FROM {table_name} LIMIT 5", + limit=5 + ) + + sample_data = [] + if sample_result.success: + sample_data = sample_result.result.get('data', []) + + # 获取行数统计 + count_result = await db_tool.execute( + operation="execute_query", + user_id=str(user_id), + sql_query=f"SELECT COUNT(*) as total_rows FROM {table_name}", + limit=1 + ) + + row_count = 0 + if count_result.success and count_result.result.get('data'): + row_count = count_result.result['data'][0].get('total_rows', 0) + + return { + 'columns_info': schema_data.get('columns', []), + 'primary_keys': schema_data.get('primary_keys', []), + 'foreign_keys': schema_data.get('foreign_keys', []), + 'indexes': schema_data.get('indexes', []), + 'sample_data': sample_data, + 'row_count': row_count, + 'table_comment': schema_data.get('table_comment', '') + } + + async def _save_table_metadata( + self, + user_id: int, + database_config_id: int, + table_name: str, + metadata: Dict[str, Any] + ) -> TableMetadata: + """保存表元数据""" + + # 检查是否已存在 + existing = self.db.query(TableMetadata).filter( + and_( + TableMetadata.created_by == user_id, + TableMetadata.database_config_id == database_config_id, + TableMetadata.table_name == table_name + ) + ).first() + + if existing: + # 更新现有记录 + existing.columns_info = metadata['columns_info'] + existing.primary_keys = metadata['primary_keys'] + existing.foreign_keys = metadata['foreign_keys'] + existing.indexes = metadata['indexes'] + existing.sample_data = metadata['sample_data'] + existing.row_count = metadata['row_count'] + existing.table_comment = metadata['table_comment'] + existing.last_synced_at = datetime.utcnow() + + self.db.commit() + self.db.refresh(existing) + return existing + else: + # 创建新记录 + table_metadata = TableMetadata( + created_by=user_id, + database_config_id=database_config_id, + table_name=table_name, + table_schema='public', + table_type='BASE TABLE', + table_comment=metadata['table_comment'], + columns_info=metadata['columns_info'], + primary_keys=metadata['primary_keys'], + foreign_keys=metadata['foreign_keys'], + indexes=metadata['indexes'], + sample_data=metadata['sample_data'], + row_count=metadata['row_count'], + is_enabled_for_qa=True, + last_synced_at=datetime.utcnow() + ) + + self.db.add(table_metadata) + self.db.commit() + self.db.refresh(table_metadata) + return table_metadata + + async def save_table_metadata_config( + self, + user_id: int, + database_config_id: int, + table_names: List[str] + ) -> Dict[str, Any]: + """保存表元数据配置(简化版,只保存基本信息)""" + try: + # 获取数据库配置 + db_config = self.db.query(DatabaseConfig).filter( + and_(DatabaseConfig.id == database_config_id, DatabaseConfig.user_id == user_id) + ).first() + + if not db_config: + raise NotFoundError("数据库配置不存在") + + saved_tables = [] + failed_tables = [] + + for table_name in table_names: + try: + # 检查是否已存在 + existing = self.db.query(TableMetadata).filter( + and_( + TableMetadata.user_id == user_id, + TableMetadata.database_config_id == database_config_id, + TableMetadata.table_name == table_name + ) + ).first() + + if existing: + # 更新现有记录 + existing.is_enabled_for_qa = True + existing.last_synced_at = datetime.utcnow() + saved_tables.append({ + 'table_name': table_name, + 'action': 'updated' + }) + else: + # 创建新记录 + metadata = TableMetadata( + created_by=user_id, + database_config_id=database_config_id, + table_name=table_name, + table_schema='public', # 默认值 + table_type='table', # 默认值 + table_comment='', + columns_count=0, # 后续可通过collect接口更新 + row_count=0, # 后续可通过collect接口更新 + is_enabled_for_qa=True, + qa_description='', + business_context='', + sample_data='{}', + column_info='{}', + last_synced_at=datetime.utcnow() + ) + + self.db.add(metadata) + saved_tables.append({ + 'table_name': table_name, + 'action': 'created' + }) + + except Exception as e: + logger.error(f"保存表 {table_name} 配置失败: {str(e)}") + failed_tables.append({ + 'table_name': table_name, + 'error': str(e) + }) + + # 提交事务 + self.db.commit() + + return { + 'saved_tables': saved_tables, + 'failed_tables': failed_tables, + 'total_saved': len(saved_tables), + 'total_failed': len(failed_tables) + } + + except Exception as e: + self.db.rollback() + logger.error(f"保存表元数据配置失败: {str(e)}") + raise e + + def get_user_table_metadata( + self, + user_id: int, + database_config_id: Optional[int] = None + ) -> List[TableMetadata]: + """获取用户的表元数据列表""" + query = self.db.query(TableMetadata).filter(TableMetadata.created_by == user_id) + + if database_config_id: + query = query.filter(TableMetadata.database_config_id == database_config_id) + else: + raise NotFoundError("数据库配置不存在") + return query.filter(TableMetadata.is_enabled_for_qa == True).all() + + def get_table_metadata_by_name( + self, + user_id: int, + database_config_id: int, + table_name: str + ) -> Optional[TableMetadata]: + """根据表名获取表元数据""" + return self.db.query(TableMetadata).filter( + and_( + TableMetadata.created_by == user_id, + TableMetadata.database_config_id == database_config_id, + TableMetadata.table_name == table_name + ) + ).first() + + def update_table_qa_settings( + self, + user_id: int, + metadata_id: int, + settings: Dict[str, Any] + ) -> bool: + """更新表的问答设置""" + try: + metadata = self.db.query(TableMetadata).filter( + and_( + TableMetadata.id == metadata_id, + TableMetadata.created_by == user_id + ) + ).first() + + if not metadata: + return False + + if 'is_enabled_for_qa' in settings: + metadata.is_enabled_for_qa = settings['is_enabled_for_qa'] + if 'qa_description' in settings: + metadata.qa_description = settings['qa_description'] + if 'business_context' in settings: + metadata.business_context = settings['business_context'] + + self.db.commit() + return True + + except Exception as e: + logger.error(f"更新表问答设置失败: {str(e)}") + self.db.rollback() + return False + + def save_table_metadata( + self, + user_id: int, + database_config_id: int, + table_name: str, + columns_info: List[Dict[str, Any]], + primary_keys: List[str], + row_count: int, + table_comment: str = '' + ) -> TableMetadata: + """保存单个表的元数据""" + try: + # 检查是否已存在 + existing = self.db.query(TableMetadata).filter( + and_( + TableMetadata.created_by == user_id, + TableMetadata.database_config_id == database_config_id, + TableMetadata.table_name == table_name + ) + ).first() + + if existing: + # 更新现有记录 + existing.columns_info = columns_info + existing.primary_keys = primary_keys + existing.row_count = row_count + existing.table_comment = table_comment + existing.last_synced_at = datetime.utcnow() + self.db.commit() + return existing + else: + # 创建新记录 + metadata = TableMetadata( + created_by=user_id, + database_config_id=database_config_id, + table_name=table_name, + table_schema='public', + table_type='BASE TABLE', + table_comment=table_comment, + columns_info=columns_info, + primary_keys=primary_keys, + row_count=row_count, + is_enabled_for_qa=True, + last_synced_at=datetime.utcnow() + ) + + self.db.add(metadata) + self.db.commit() + self.db.refresh(metadata) + return metadata + + except Exception as e: + logger.error(f"保存表元数据失败: {str(e)}") + self.db.rollback() + raise e + + def _decrypt_password(self, encrypted_password: str) -> str: + """解密密码(需要实现加密逻辑)""" + # 这里需要实现与DatabaseConfigService相同的解密逻辑 + # 暂时返回原始密码 + return encrypted_password \ No newline at end of file diff --git a/backend/th_agenter/services/tools/__init__.py b/backend/th_agenter/services/tools/__init__.py new file mode 100644 index 0000000..e2077e3 --- /dev/null +++ b/backend/th_agenter/services/tools/__init__.py @@ -0,0 +1,36 @@ +"""Agent tools package.""" + +from .weather import WeatherQueryTool +from .search import TavilySearchTool +from .datetime_tool import DateTimeTool + +# Try to import PostgreSQL MCP tool +try: + from th_agenter.services.mcp.postgresql_mcp import PostgreSQLMCPTool +except ImportError: + PostgreSQLMCPTool = None + +# Try to import MySQL MCP tool +try: + from th_agenter.services.mcp.mysql_mcp import MySQLMCPTool +except ImportError: + MySQLMCPTool = None + + +# Try to import LangChain native tools if available +try: + from .langchain_native_tools import LANGCHAIN_NATIVE_TOOLS +except ImportError: + LANGCHAIN_NATIVE_TOOLS = [] + +__all__ = [ + 'WeatherQueryTool', + 'TavilySearchTool', + 'DateTimeTool', +] + ([ + 'PostgreSQLMCPTool', +] if PostgreSQLMCPTool is not None else []) + ([ + 'MySQLMCPTool', +] if MySQLMCPTool is not None else []) + [ + 'LANGCHAIN_NATIVE_TOOLS' +] \ No newline at end of file diff --git a/backend/th_agenter/services/tools/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/services/tools/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4ed394774c0e08a7d2cf0b422a03d67ecb545040 GIT binary patch literal 988 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 CQlVo2 literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/tools/__pycache__/weather.cpython-313.pyc b/backend/th_agenter/services/tools/__pycache__/weather.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..b6056dda0460ed1fd9ecd13c7b40edf09a659f9d GIT binary patch literal 4635 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 literal 0 HcmV?d00001 diff --git a/backend/th_agenter/services/tools/datetime_tool.py b/backend/th_agenter/services/tools/datetime_tool.py new file mode 100644 index 0000000..65bf120 --- /dev/null +++ b/backend/th_agenter/services/tools/datetime_tool.py @@ -0,0 +1,186 @@ +from pydantic import BaseModel, Field +from typing import Optional, Type, Literal, ClassVar +import datetime +import pytz +import logging + +logger = logging.getLogger("datetime_tool") + +# Try to import BaseTool from langchain_core +try: + from langchain_core.tools import BaseTool +except ImportError: + logger.warning("langchain_core not available. DateTimeTool will be disabled.") + BaseTool = None + +# 定义输入参数模型(使用Pydantic替代原get_parameters()) +class DateTimeInput(BaseModel): + operation: Literal["current_time", "timezone_convert", "date_diff", "add_time", "format_date"] = Field( + description="操作类型: current_time(当前时间), timezone_convert(时区转换), " + "date_diff(日期差), add_time(时间加减), format_date(格式化日期)" + ) + timezone: Optional[str] = Field( + default="UTC", + description="时区名称 (e.g., 'UTC', 'Asia/Shanghai')" + ) + date_string: Optional[str] = Field( + description="日期字符串 (格式: YYYY-MM-DD 或 YYYY-MM-DD HH:MM:SS)" + ) + target_timezone: Optional[str] = Field( + description="目标时区(用于时区转换)" + ) + days: Optional[int] = Field( + default=0, + description="要加减的天数" + ) + hours: Optional[int] = Field( + default=0, + description="要加减的小时数" + ) + format: Optional[str] = Field( + default="%Y-%m-%d %H:%M:%S", + description="日期格式字符串 (e.g., '%Y-%m-%d %H:%M:%S')" + ) + +class DateTimeTool(BaseTool): + """日期时间操作工具(支持时区转换、日期计算等)""" + + name: ClassVar[str] = "datetime_tool" + description: ClassVar[str] = """执行日期时间相关操作,包括: + - 获取当前时间 + - 时区转换 + - 计算日期差 + - 日期时间加减 + - 格式化日期 + 使用时必须指定operation参数确定操作类型。""" + args_schema: Type[BaseModel] = DateTimeInput + + def _parse_datetime(self, date_string: str, timezone_str: str = "UTC") -> datetime.datetime: + """解析日期字符串(私有方法)""" + tz = pytz.timezone(timezone_str) + formats = [ + "%Y-%m-%d %H:%M:%S", + "%Y-%m-%d %H:%M", + "%Y-%m-%d", + "%Y/%m/%d %H:%M:%S", + "%Y/%m/%d", + "%d/%m/%Y %H:%M:%S", + "%d/%m/%Y", + "%m/%d/%Y %H:%M:%S", + "%m/%d/%Y" + ] + + for fmt in formats: + try: + dt = datetime.datetime.strptime(date_string, fmt) + return tz.localize(dt) + except ValueError: + continue + raise ValueError(f"无法解析日期字符串: {date_string}") + + def _run(self, + operation: str, + timezone: str = "UTC", + date_string: Optional[str] = None, + target_timezone: Optional[str] = None, + days: int = 0, + hours: int = 0, + format: str = "%Y-%m-%d %H:%M:%S") -> dict: + """同步执行日期时间操作""" + logger.info(f"执行日期时间操作: {operation}") + + try: + if operation == "current_time": + tz = pytz.timezone(timezone) + now = datetime.datetime.now(tz) + return { + "status": "success", + "result": { + "formatted": now.strftime(format), + "iso": now.isoformat(), + "timestamp": now.timestamp(), + "timezone": timezone + }, + "summary": f"当前时间 ({timezone}): {now.strftime(format)}" + } + + elif operation == "timezone_convert": + if not date_string or not target_timezone: + raise ValueError("必须提供date_string和target_timezone参数") + + source_dt = self._parse_datetime(date_string, timezone) + target_dt = source_dt.astimezone(pytz.timezone(target_timezone)) + + return { + "status": "success", + "result": { + "source": source_dt.strftime(format), + "target": target_dt.strftime(format), + "source_tz": timezone, + "target_tz": target_timezone + }, + "summary": f"时区转换: {source_dt.strftime(format)} → {target_dt.strftime(format)}" + } + + elif operation == "date_diff": + if not date_string: + raise ValueError("必须提供date_string参数") + + target_dt = self._parse_datetime(date_string, timezone) + current_dt = datetime.datetime.now(pytz.timezone(timezone)) + delta = target_dt - current_dt + + return { + "status": "success", + "result": { + "days": delta.days, + "hours": delta.seconds // 3600, + "total_seconds": delta.total_seconds(), + "is_future": delta.days > 0 + }, + "summary": f"日期差: {abs(delta.days)}天 {delta.seconds//3600}小时" + } + + elif operation == "add_time": + base_dt = self._parse_datetime(date_string, timezone) if date_string \ + else datetime.datetime.now(pytz.timezone(timezone)) + new_dt = base_dt + datetime.timedelta(days=days, hours=hours) + + return { + "status": "success", + "result": { + "original": base_dt.strftime(format), + "new": new_dt.strftime(format), + "delta": f"{days}天 {hours}小时" + }, + "summary": f"时间计算: {base_dt.strftime(format)} + {days}天 {hours}小时 = {new_dt.strftime(format)}" + } + + elif operation == "format_date": + dt = self._parse_datetime(date_string, timezone) if date_string \ + else datetime.datetime.now(pytz.timezone(timezone)) + formatted = dt.strftime(format) + + return { + "status": "success", + "result": { + "original": dt.isoformat(), + "formatted": formatted + }, + "summary": f"格式化结果: {formatted}" + } + + else: + raise ValueError(f"未知操作类型: {operation}") + + except Exception as e: + logger.error(f"操作失败: {str(e)}") + return { + "status": "error", + "message": str(e), + "operation": operation + } + + async def _arun(self, **kwargs): + """异步执行""" + return self._run(**kwargs) \ No newline at end of file diff --git a/backend/th_agenter/services/tools/search.py b/backend/th_agenter/services/tools/search.py new file mode 100644 index 0000000..9c91512 --- /dev/null +++ b/backend/th_agenter/services/tools/search.py @@ -0,0 +1,92 @@ +"""基于TavilySearch的搜索工具""" + +from th_agenter.core.config import get_settings +from th_agenter.utils.logger import get_logger + +logger = get_logger("search_tool") + +from pydantic import BaseModel, Field, PrivateAttr +from typing import Optional, Type, ClassVar +import logging + +logger = logging.getLogger(__name__) + +# Try to import BaseTool from langchain_core +try: + from langchain_core.tools import BaseTool +except ImportError: + logger.warning("langchain_core not available. SearchTool will be disabled.") + BaseTool = None + +# Try to import TavilySearchResults +try: + from langchain_community.tools.tavily_search import TavilySearchResults +except ImportError: + logger.warning("langchain_community not available. TavilySearchTool will be disabled.") + TavilySearchResults = None + + +# 定义输入参数模型(替代原get_parameters()) +class SearchInput(BaseModel): + query: str = Field(description="搜索查询内容") + max_results: Optional[int] = Field( + default=5, + description="返回结果的最大数量(默认:5)" + ) + topic: Optional[str] = Field( + default="general", + description="搜索主题,可选值:general, academic, news, places" + ) + + +class TavilySearchTool(BaseTool): + name:ClassVar[str] = "tavily_search_tool" + description:ClassVar[str] = """使用Tavily搜索引擎进行网络搜索,可以获取最新信息。 + 输入应该包含搜索查询(query),可选参数包括max_results和topic。""" # 替代get_description() + args_schema: Type[BaseModel] = SearchInput # 用Pydantic模型定义参数 + _tavily_api_key: str = PrivateAttr() + _search_client: TavilySearchResults = PrivateAttr() + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._tavily_api_key = get_settings().tool.tavily_api_key + if not self._tavily_api_key: + raise ValueError("Tavily API key not found in settings") + + # 初始化Tavily客户端 + self._search_client = TavilySearchResults( + tavily_api_key=self._tavily_api_key + ) + + def _run(self, query: str, max_results: int = 5, topic: str = "general"): + try: + logger.info(f"执行搜索:{query}") + # 调用Tavily(LangChain已内置Tavily工具,这里直接使用) + results = self._search_client.run({ + "query": query, + "max_results": max_results, + "topic": topic + }) + + # 格式化结果(根据Tavily的实际返回结构调整) + if isinstance(results, list): + return { + "status": "success", + "results": [ + { + "title": r.get("title", ""), + "url": r.get("url", ""), + "content": r.get("content", "")[:200] + "..." + } for r in results + ] + } + else: + return {"status": "error", "message": "Unexpected result format"} + + except Exception as e: + logger.error(f"搜索失败: {str(e)}") + return {"status": "error", "message": str(e)} + + async def _arun(self, **kwargs): + """异步版本""" + """直接调用同步版本""" + return self._run(**kwargs) # 直接委托给同步方法 \ No newline at end of file diff --git a/backend/th_agenter/services/tools/weather.py b/backend/th_agenter/services/tools/weather.py new file mode 100644 index 0000000..3d345c4 --- /dev/null +++ b/backend/th_agenter/services/tools/weather.py @@ -0,0 +1,85 @@ +from pydantic import BaseModel, Field, PrivateAttr +from typing import Optional, Type, ClassVar +import requests +import logging +from th_agenter.core.config import get_settings + +logger = logging.getLogger("weather_tool") + +# Try to import BaseTool from langchain_core +try: + from langchain_core.tools import BaseTool +except ImportError: + logger.warning("langchain_core not available. WeatherQueryTool will be disabled.") + BaseTool = None + +# 定义输入参数模型(替代原get_parameters()) +class WeatherInput(BaseModel): + location: str = Field( + description="城市名称,例如:'北京',只能是单个城市", + examples=["北京", "上海", "New York"] + ) + +class WeatherQueryTool(BaseTool): + """心知天气API查询工具(LangChain标准版)""" + name: ClassVar[str] = "weather_query_tool" + description: ClassVar[str] = """通过心知天气API查询实时天气数据。获取指定城市的当前天气信息,包括温度、湿度、天气状况等。""" + args_schema: Type[BaseModel] = WeatherInput # 参数规范 + # 使用PrivateAttr声明不参与验证的私有属性 + _api_key: str = PrivateAttr() + _base_params: dict = PrivateAttr() + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self._api_key = get_settings().tool.weather_api_key + if not self._api_key: + raise ValueError("Weather API key not found in settings") + + # 基础请求参数 + self._base_params = { + "key": self._api_key, + "language": "zh-Hans", + "unit": "c" + } + + def _run(self, location: str) -> dict: + """同步执行天气查询""" + try: + logger.info(f"查询天气 - 城市: {location}") + + # 构建API请求 + url = "https://api.seniverse.com/v3/weather/now.json" + params = {**self._base_params, "location": location} + + response = requests.get(url, params=params, timeout=10) + response.raise_for_status() + + data = response.json() + + # 处理API响应 + if 'results' not in data: + error_msg = data.get('status', 'API返回格式异常') + raise ValueError(f"天气API错误: {error_msg}") + + weather = data['results'][0]['now'] + return { + "status": "success", + "location": location, + "temperature": weather["temperature"], + "condition": weather["text"], + "humidity": weather.get("humidity", "N/A"), + "wind": weather.get("wind_direction", "N/A"), + "full_data": weather + } + + except requests.exceptions.RequestException as e: + logger.error(f"网络请求失败: {str(e)}") + return {"status": "error", "message": f"网络错误: {str(e)}"} + except Exception as e: + logger.error(f"查询失败: {str(e)}") + return {"status": "error", "message": str(e)} + + async def _arun(self, location: str) -> dict: + """异步执行(示例实现)""" + # 实际项目中可以用aiohttp替换requests + return self._run(location) \ No newline at end of file diff --git a/backend/th_agenter/services/user.py b/backend/th_agenter/services/user.py new file mode 100644 index 0000000..a5c456f --- /dev/null +++ b/backend/th_agenter/services/user.py @@ -0,0 +1,282 @@ +"""User service for managing user operations.""" + +from typing import Optional, List, Tuple +from sqlalchemy.orm import Session +from sqlalchemy import or_, and_, desc + +from ..models.user import User +from ..utils.schemas import UserCreate, UserUpdate +from ..utils.exceptions import DatabaseError, ValidationError +from ..utils.logger import get_logger +from .auth import AuthService + +logger = get_logger(__name__) + + +class UserService: + """Service for user management operations.""" + + def __init__(self, db: Session): + self.db = db + + def get_password_hash(self, password: str) -> str: + """Hash a password.""" + return AuthService.get_password_hash(password) + + def verify_password(self, plain_password: str, hashed_password: str) -> bool: + """Verify a password against its hash.""" + return AuthService.verify_password(plain_password, hashed_password) + + def get_user_by_id(self, user_id: int) -> Optional[User]: + """Get user by ID.""" + try: + # Use options to avoid loading problematic relationships + from sqlalchemy.orm import noload + return self.db.query(User).options( + noload(User.roles) + ).filter(User.id == user_id).first() + except Exception as e: + logger.error(f"Error getting user by ID {user_id}: {e}") + raise DatabaseError(f"Failed to get user: {str(e)}") + + def get_user(self, user_id: int) -> Optional[User]: + """Get user by ID (alias for get_user_by_id).""" + return self.get_user_by_id(user_id) + + def get_user_by_email(self, email: str) -> Optional[User]: + """Get user by email.""" + try: + return self.db.query(User).filter(User.email == email).first() + except Exception as e: + logger.error(f"Error getting user by email {email}: {e}") + raise DatabaseError(f"Failed to get user: {str(e)}") + + def get_user_by_username(self, username: str) -> Optional[User]: + """Get user by username.""" + try: + return self.db.query(User).filter(User.username == username).first() + except Exception as e: + logger.error(f"Error getting user by username {username}: {e}") + raise DatabaseError(f"Failed to get user: {str(e)}") + + def create_user(self, user_data: UserCreate) -> User: + """Create a new user.""" + try: + # Validate input + if len(user_data.password) < 6: + raise ValidationError("Password must be at least 6 characters long") + if len(user_data.password) > 72: + raise ValidationError("Password cannot be longer than 72 characters") + + # Hash password + hashed_password = self.get_password_hash(user_data.password) + + # Create user + db_user = User( + username=user_data.username, + email=user_data.email, + hashed_password=hashed_password, + full_name=user_data.full_name, + is_active=True + ) + + # Set audit fields + db_user.set_audit_fields() + + self.db.add(db_user) + self.db.commit() + self.db.refresh(db_user) + + logger.info(f"User created successfully: {user_data.username}") + return db_user + + except ValidationError: + raise + except Exception as e: + self.db.rollback() + logger.error(f"Error creating user {user_data.username}: {e}") + raise DatabaseError(f"Failed to create user: {str(e)}") + + def update_user(self, user_id: int, user_update: UserUpdate) -> Optional[User]: + """Update user information.""" + try: + user = self.get_user_by_id(user_id) + if not user: + return None + + # Update fields + update_data = user_update.dict(exclude_unset=True) + + if "password" in update_data: + update_data["hashed_password"] = self.get_password_hash(update_data.pop("password")) + + for field, value in update_data.items(): + setattr(user, field, value) + + # Skip audit fields for now due to database schema mismatch + # user.set_audit_fields(is_update=True) + + self.db.commit() + self.db.refresh(user) + + logger.info(f"User updated successfully: {user.username}") + return user + + except Exception as e: + self.db.rollback() + logger.error(f"Error updating user {user_id}: {e}") + raise DatabaseError(f"Failed to update user: {str(e)}") + + def get_users(self, skip: int = 0, limit: int = 100) -> List[User]: + """Get all users with pagination.""" + try: + return self.db.query(User).offset(skip).limit(limit).all() + except Exception as e: + logger.error(f"Error getting users: {e}") + raise DatabaseError(f"Failed to get users: {str(e)}") + + def get_users_with_filters( + self, + skip: int = 0, + limit: int = 100, + search: Optional[str] = None, + role_id: Optional[int] = None, + is_active: Optional[bool] = None + ) -> Tuple[List[User], int]: + """Get users with filters and return total count.""" + try: + query = self.db.query(User).order_by(desc('created_at')) + + # Apply filters + if search: + search_term = f"%{search}%" + query = query.filter( + or_( + User.username.ilike(search_term), + User.email.ilike(search_term), + User.full_name.ilike(search_term) + ) + ) + + if role_id is not None: + from ..models.permission import UserRole + query = query.join(UserRole).filter(UserRole.role_id == role_id) + + if is_active is not None: + query = query.filter(User.is_active == is_active) + + # Get total count + total = query.count() + + # Apply pagination + users = query.offset(skip).limit(limit).all() + + return users, total + + except Exception as e: + logger.error(f"Error getting users with filters: {e}") + raise DatabaseError(f"Failed to get users: {str(e)}") + + def delete_user(self, user_id: int) -> bool: + """Delete a user.""" + try: + user = self.get_user_by_id(user_id) + if not user: + return False + + # Manually delete related records to avoid cascade issues + from sqlalchemy import text + + # Delete user_roles records + self.db.execute(text("DELETE FROM user_roles WHERE user_id = :user_id"), {"user_id": user_id}) + + # Now delete the user + self.db.delete(user) + self.db.commit() + + logger.info(f"User deleted successfully: {user.username}") + return True + + except Exception as e: + self.db.rollback() + logger.error(f"Error deleting user {user_id}: {e}") + raise DatabaseError(f"Failed to delete user: {str(e)}") + + def authenticate_user(self, username: str, password: str) -> Optional[User]: + """Authenticate user with username and password.""" + try: + user = self.get_user_by_username(username) + if not user: + return None + + if not self.verify_password(password, user.hashed_password): + return None + + if not user.is_active: + return None + + return user + + except Exception as e: + logger.error(f"Error authenticating user {username}: {e}") + return None + + def change_password(self, user_id: int, current_password: str, new_password: str) -> bool: + """Change user password.""" + try: + user = self.get_user_by_id(user_id) + if not user: + raise ValidationError("User not found") + + # Verify current password + if not self.verify_password(current_password, user.hashed_password): + raise ValidationError("Current password is incorrect") + + # Validate new password + if len(new_password) < 6: + raise ValidationError("New password must be at least 6 characters long") + + # Hash new password + hashed_password = self.get_password_hash(new_password) + + # Update password + user.hashed_password = hashed_password + self.db.commit() + + logger.info(f"Password changed successfully for user: {user.username}") + return True + + except ValidationError: + raise + except Exception as e: + self.db.rollback() + logger.error(f"Error changing password for user {user_id}: {e}") + raise DatabaseError(f"Failed to change password: {str(e)}") + + def reset_password(self, user_id: int, new_password: str) -> bool: + """Reset user password (admin only, no current password required).""" + try: + user = self.get_user_by_id(user_id) + if not user: + raise ValidationError("User not found") + + # Validate new password + if len(new_password) < 6: + raise ValidationError("New password must be at least 6 characters long") + + # Hash new password + hashed_password = self.get_password_hash(new_password) + + # Update password + user.hashed_password = hashed_password + self.db.commit() + + logger.info(f"Password reset successfully for user: {user.username}") + return True + + except ValidationError: + raise + except Exception as e: + self.db.rollback() + logger.error(f"Error resetting password for user {user_id}: {e}") + raise DatabaseError(f"Failed to reset password: {str(e)}") \ No newline at end of file diff --git a/backend/th_agenter/services/workflow_engine.py b/backend/th_agenter/services/workflow_engine.py new file mode 100644 index 0000000..edb200e --- /dev/null +++ b/backend/th_agenter/services/workflow_engine.py @@ -0,0 +1,924 @@ +"""Workflow execution engine.""" + +import asyncio +import json +import time +from datetime import datetime +from typing import Dict, Any, Optional, List +from sqlalchemy.orm import Session + +from ..models.workflow import Workflow, WorkflowExecution, NodeExecution, ExecutionStatus, NodeType +from ..models.llm_config import LLMConfig +from ..services.llm_service import LLMService + +from ..db.database import get_db +from ..utils.logger import get_logger + +logger = get_logger("workflow_engine") + + +class WorkflowEngine: + """工作流执行引擎""" + + def __init__(self, db: Session): + self.db = db + self.llm_service = LLMService() + + + async def execute_workflow(self, workflow: Workflow, input_data: Optional[Dict[str, Any]] = None, + user_id: int = None, db: Session = None) -> 'WorkflowExecutionResponse': + """执行工作流""" + from ..schemas.workflow import WorkflowExecutionResponse + + if db: + self.db = db + + # 创建执行记录 + execution = WorkflowExecution( + workflow_id=workflow.id, + status=ExecutionStatus.RUNNING, + input_data=input_data or {}, + executor_id=user_id, + started_at=datetime.now().isoformat() + ) + execution.set_audit_fields(user_id) + + self.db.add(execution) + self.db.commit() + self.db.refresh(execution) + + + + try: + # 解析工作流定义 + definition = workflow.definition + nodes = {node['id']: node for node in definition['nodes']} + connections = definition['connections'] + + # 构建节点依赖图 + node_graph = self._build_node_graph(nodes, connections) + + # 执行工作流 + result = await self._execute_nodes(execution, nodes, node_graph, input_data or {}) + + # 更新执行状态 + execution.status = ExecutionStatus.COMPLETED + execution.output_data = result + execution.completed_at = datetime.now().isoformat() + + + + except Exception as e: + logger.error(f"工作流执行失败: {str(e)}") + execution.status = ExecutionStatus.FAILED + execution.error_message = str(e) + execution.completed_at = datetime.now().isoformat() + + + + execution.set_audit_fields(user_id, is_update=True) + self.db.commit() + self.db.refresh(execution) + + return WorkflowExecutionResponse.from_orm(execution) + + async def execute_workflow_stream(self, workflow: 'Workflow', input_data: Optional[Dict[str, Any]] = None, + user_id: int = None, db: Session = None): + """流式执行工作流,实时推送节点状态""" + from ..schemas.workflow import WorkflowExecutionResponse + from typing import AsyncGenerator + + if db: + self.db = db + + # 创建执行记录 + execution = WorkflowExecution( + workflow_id=workflow.id, + status=ExecutionStatus.RUNNING, + input_data=input_data or {}, + executor_id=user_id, + started_at=datetime.now().isoformat() + ) + execution.set_audit_fields(user_id) + + self.db.add(execution) + self.db.commit() + self.db.refresh(execution) + + # 发送工作流开始执行的消息 + yield { + 'type': 'workflow_status', + 'execution_id': execution.id, + 'status': 'started', + 'data': { + "workflow_id": workflow.id, + "workflow_name": workflow.name, + "input_data": input_data or {}, + "started_at": execution.started_at + }, + 'timestamp': datetime.now().isoformat() + } + + try: + # 解析工作流定义 + definition = workflow.definition + nodes = {node['id']: node for node in definition['nodes']} + connections = definition['connections'] + + # 构建节点依赖图 + node_graph = self._build_node_graph(nodes, connections) + + # 执行工作流(流式版本) + result = None + async for step_data in self._execute_nodes_stream(execution, nodes, node_graph, input_data or {}): + yield step_data + # 如果是最终结果,保存它 + if step_data.get('type') == 'workflow_result': + result = step_data.get('data', {}) + + # 更新执行状态 + execution.status = ExecutionStatus.COMPLETED + execution.output_data = result + execution.completed_at = datetime.now().isoformat() + + # 发送工作流完成的消息 + yield { + 'type': 'workflow_status', + 'execution_id': execution.id, + 'status': 'completed', + 'data': { + "output_data": result, + "completed_at": execution.completed_at + }, + 'timestamp': datetime.now().isoformat() + } + + except Exception as e: + logger.error(f"工作流执行失败: {str(e)}") + execution.status = ExecutionStatus.FAILED + execution.error_message = str(e) + execution.completed_at = datetime.now().isoformat() + + # 发送工作流失败的消息 + yield { + 'type': 'workflow_status', + 'execution_id': execution.id, + 'status': 'failed', + 'data': { + "error_message": str(e), + "completed_at": execution.completed_at + }, + 'timestamp': datetime.now().isoformat() + } + + execution.set_audit_fields(user_id, is_update=True) + self.db.commit() + self.db.refresh(execution) + + def _build_node_graph(self, nodes: Dict[str, Any], connections: List[Dict[str, Any]]) -> Dict[str, Dict[str, Any]]: + """构建节点依赖图""" + graph = {} + + for node_id, node in nodes.items(): + graph[node_id] = { + 'node': node, + 'inputs': [], # 输入节点 + 'outputs': [] # 输出节点 + } + + for connection in connections: + # 支持两种字段名格式:from/to 和 from_node/to_node + from_node = connection.get('from') or connection.get('from_node') + to_node = connection.get('to') or connection.get('to_node') + + if from_node in graph and to_node in graph: + graph[from_node]['outputs'].append(to_node) + graph[to_node]['inputs'].append(from_node) + + return graph + + async def _execute_nodes(self, execution: WorkflowExecution, nodes: Dict[str, Any], + node_graph: Dict[str, Dict[str, Any]], workflow_input: Dict[str, Any]) -> Dict[str, Any]: + """执行节点""" + # 找到开始节点 + start_nodes = [node_id for node_id, info in node_graph.items() + if info['node']['type'] == 'start'] + + if not start_nodes: + raise ValueError("未找到开始节点") + + if len(start_nodes) > 1: + raise ValueError("存在多个开始节点") + + start_node_id = start_nodes[0] + + # 执行上下文 + context = { + 'workflow_input': workflow_input, + 'node_outputs': {} + } + + # 从开始节点开始执行 + await self._execute_node_recursive(execution, start_node_id, node_graph, context) + + # 找到结束节点的输出作为工作流结果 + end_nodes = [node_id for node_id, info in node_graph.items() + if info['node']['type'] == 'end'] + + if end_nodes: + end_node_id = end_nodes[0] + return context['node_outputs'].get(end_node_id, {}) + + return {} + + async def _execute_nodes_stream(self, execution: WorkflowExecution, nodes: Dict[str, Any], + node_graph: Dict[str, Dict[str, Any]], workflow_input: Dict[str, Any]): + """流式执行节点,实时推送节点状态""" + # 找到开始节点 + start_nodes = [node_id for node_id, info in node_graph.items() + if info['node']['type'] == 'start'] + + if not start_nodes: + raise ValueError("未找到开始节点") + + if len(start_nodes) > 1: + raise ValueError("存在多个开始节点") + + start_node_id = start_nodes[0] + + # 执行上下文 + context = { + 'workflow_input': workflow_input, + 'node_outputs': {} + } + + # 从开始节点开始执行 + async for step_data in self._execute_node_recursive_stream(execution, start_node_id, node_graph, context): + yield step_data + + # 找到结束节点的输出作为工作流结果 + end_nodes = [node_id for node_id, info in node_graph.items() + if info['node']['type'] == 'end'] + + if end_nodes: + end_node_id = end_nodes[0] + result = context['node_outputs'].get(end_node_id, {}) + else: + result = {} + + # 发送最终结果 + yield { + 'type': 'workflow_result', + 'execution_id': execution.id, + 'data': result, + 'timestamp': datetime.now().isoformat() + } + + async def _execute_node_recursive_stream(self, execution: WorkflowExecution, node_id: str, + node_graph: Dict[str, Dict[str, Any]], context: Dict[str, Any]): + """递归执行节点(流式版本)""" + if node_id in context['node_outputs']: + # 节点已执行过 + return + + node_info = node_graph[node_id] + node = node_info['node'] + + # 等待所有输入节点完成 + for input_node_id in node_info['inputs']: + async for step_data in self._execute_node_recursive_stream(execution, input_node_id, node_graph, context): + yield step_data + + # 发送节点开始执行的消息 + yield { + 'type': 'node_status', + 'execution_id': execution.id, + 'node_id': node_id, + 'status': 'started', + 'data': { + 'node_name': node.get('name', ''), + 'node_type': node.get('type', ''), + 'started_at': datetime.now().isoformat() + }, + 'timestamp': datetime.now().isoformat() + } + + try: + # 执行当前节点 + output = await self._execute_single_node(execution, node, context) + context['node_outputs'][node_id] = output + + # 发送节点完成的消息 + yield { + 'type': 'node_status', + 'execution_id': execution.id, + 'node_id': node_id, + 'status': 'completed', + 'data': { + 'node_name': node.get('name', ''), + 'node_type': node.get('type', ''), + 'output': output, + 'completed_at': datetime.now().isoformat() + }, + 'timestamp': datetime.now().isoformat() + } + + except Exception as e: + # 发送节点失败的消息 + yield { + 'type': 'node_status', + 'execution_id': execution.id, + 'node_id': node_id, + 'status': 'failed', + 'data': { + 'node_name': node.get('name', ''), + 'node_type': node.get('type', ''), + 'error_message': str(e), + 'failed_at': datetime.now().isoformat() + }, + 'timestamp': datetime.now().isoformat() + } + raise + + # 执行所有输出节点 + for output_node_id in node_info['outputs']: + async for step_data in self._execute_node_recursive_stream(execution, output_node_id, node_graph, context): + yield step_data + + async def _execute_node_recursive(self, execution: WorkflowExecution, node_id: str, + node_graph: Dict[str, Dict[str, Any]], context: Dict[str, Any]): + """递归执行节点""" + if node_id in context['node_outputs']: + # 节点已执行过 + return + + node_info = node_graph[node_id] + node = node_info['node'] + + # 等待所有输入节点完成 + for input_node_id in node_info['inputs']: + await self._execute_node_recursive(execution, input_node_id, node_graph, context) + + # 执行当前节点 + output = await self._execute_single_node(execution, node, context) + context['node_outputs'][node_id] = output + + # 执行所有输出节点 + for output_node_id in node_info['outputs']: + await self._execute_node_recursive(execution, output_node_id, node_graph, context) + + async def _execute_single_node(self, execution: WorkflowExecution, node: Dict[str, Any], + context: Dict[str, Any]) -> Dict[str, Any]: + """执行单个节点""" + node_id = node['id'] + node_type = node['type'] + node_name = node['name'] + + # 创建节点执行记录 + node_execution = NodeExecution( + workflow_execution_id=execution.id, + node_id=node_id, + node_type=NodeType(node_type), + node_name=node_name, + status=ExecutionStatus.RUNNING, + started_at=datetime.now().isoformat() + ) + self.db.add(node_execution) + self.db.commit() + self.db.refresh(node_execution) + + start_time = time.time() + + try: + # 准备输入数据 + input_data = self._prepare_node_input(node, context) + + # 为前端显示准备输入数据 + display_input_data = input_data.copy() + + # 对于开始节点,显示的输入应该是workflow_input + if node_type == 'start': + display_input_data = input_data['workflow_input'] + elif node_type == 'llm': + # 对于LLM节点,先执行变量替换以获取处理后的提示词 + config = input_data['node_config'] + prompt_template = config.get('prompt', '') + enable_variable_substitution = config.get('enable_variable_substitution', True) + + if enable_variable_substitution: + processed_prompt = self._substitute_variables(prompt_template, input_data) + else: + processed_prompt = prompt_template + + display_input_data = { + 'original_prompt': prompt_template, + 'processed_prompt': processed_prompt, + 'model_config': config, + 'resolved_inputs': input_data.get('resolved_inputs', {}) + } + + node_execution.input_data = display_input_data + self.db.commit() + + + + # 根据节点类型执行 + if node_type == 'start': + output_data = await self._execute_start_node(node, input_data) + elif node_type == 'end': + output_data = await self._execute_end_node(node, input_data) + elif node_type == 'llm': + output_data = await self._execute_llm_node(node, input_data) + elif node_type == 'condition': + output_data = await self._execute_condition_node(node, input_data) + elif node_type == 'code': + output_data = await self._execute_code_node(node, input_data) + elif node_type == 'http': + output_data = await self._execute_http_node(node, input_data) + else: + raise ValueError(f"不支持的节点类型: {node_type}") + + # 更新执行状态 + end_time = time.time() + node_execution.status = ExecutionStatus.COMPLETED + node_execution.output_data = output_data + node_execution.completed_at = datetime.now().isoformat() + node_execution.duration_ms = int((end_time - start_time) * 1000) + + self.db.commit() + + + + return output_data + + except Exception as e: + logger.error(f"节点 {node_id} 执行失败: {str(e)}") + end_time = time.time() + node_execution.status = ExecutionStatus.FAILED + node_execution.error_message = str(e) + node_execution.completed_at = datetime.now().isoformat() + node_execution.duration_ms = int((end_time - start_time) * 1000) + self.db.commit() + + + + raise + + def _prepare_node_input(self, node: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]: + """准备节点输入数据""" + # 基础输入数据 + input_data = { + 'workflow_input': context['workflow_input'], + 'node_config': node.get('config', {}), + 'previous_outputs': context['node_outputs'] + } + + # 处理节点参数配置 + node_parameters = node.get('parameters', {}) + if node_parameters and 'inputs' in node_parameters: + resolved_inputs = {} + + for param in node_parameters['inputs']: + param_name = param.get('name') + param_source = param.get('source', 'default') + param_default = param.get('default_value') + variable_name = param.get('variable_name', '') + + # 优先使用variable_name,如果存在的话 + if variable_name: + resolved_value = self._resolve_variable_value(variable_name, context) + resolved_inputs[param_name] = resolved_value if resolved_value is not None else param_default + elif param_source == 'workflow': + # 从工作流输入获取 + source_param_name = param.get('source_param_name', param_name) + resolved_inputs[param_name] = context['workflow_input'].get(source_param_name, param_default) + elif param_source == 'node': + # 从其他节点输出获取 + source_node_id = param.get('source_node_id') + source_param_name = param.get('source_param_name', 'data') + + if source_node_id and source_node_id in context['node_outputs']: + source_output = context['node_outputs'][source_node_id] + if isinstance(source_output, dict): + resolved_inputs[param_name] = source_output.get(source_param_name, param_default) + else: + resolved_inputs[param_name] = source_output + else: + resolved_inputs[param_name] = param_default + else: + # 使用默认值 + resolved_inputs[param_name] = param_default + + # 将解析后的参数添加到输入数据 + input_data['resolved_inputs'] = resolved_inputs + + return input_data + + def _resolve_variable_value(self, variable_name: str, context: Dict[str, Any]) -> Any: + """解析变量值,支持格式如 "node_id.output.field_name" 或更深层路径""" + try: + # 解析变量名格式:node_id.output.field_name 或 node_id.field1.field2.field3 + parts = variable_name.split('.') + if len(parts) >= 2: + source_node_id = parts[0] + + # 从previous_outputs中获取源节点的输出 + if source_node_id in context['node_outputs']: + source_output = context['node_outputs'][source_node_id] + + if isinstance(source_output, dict): + # 从第二个部分开始遍历路径 + current_value = source_output + for field_name in parts[1:]: + if isinstance(current_value, dict) and field_name in current_value: + current_value = current_value[field_name] + else: + # 如果路径不存在,返回None + return None + + return current_value + + return None + except Exception as e: + logger.warning(f"解析变量值失败: {variable_name}, 错误: {str(e)}") + return None + + async def _execute_start_node(self, node: Dict[str, Any], input_data: Dict[str, Any]) -> Dict[str, Any]: + """执行开始节点""" + # 开始节点的输入和输出应该一致,都是workflow_input + workflow_input = input_data['workflow_input'] + return { + 'success': True, + 'message': '工作流开始', + 'data': workflow_input, + 'user_input': workflow_input # 添加用户输入显示 + } + + async def _execute_end_node(self, node: Dict[str, Any], input_data: Dict[str, Any]) -> Dict[str, Any]: + """执行结束节点""" + previous_outputs = input_data.get('previous_outputs', {}) + + # 处理结束节点的输出参数配置 + node_parameters = node.get('parameters', {}) + output_params = node_parameters.get('outputs', []) + + result_data = {} + + # 根据输出参数配置获取对应的值 + for param in output_params: + param_name = param.get('name') + variable_name = param.get('variable_name') + + if variable_name: + # 解析variable_name,格式如: "node_1759022611056.output.response" + try: + parts = variable_name.split('.') + if len(parts) >= 3: + source_node_id = parts[0] + output_type = parts[1] # 通常是"output" + field_name = parts[2] # 具体的字段名,如"response" + + # 从前一个节点的输出中获取值 + if source_node_id in previous_outputs: + source_output = previous_outputs[source_node_id] + if isinstance(source_output, dict): + # 首先尝试从根级别获取字段(如LLM节点的response字段) + if field_name in source_output: + result_data[param_name] = source_output[field_name] + # 如果根级别没有,再尝试从data字段中获取 + elif 'data' in source_output and isinstance(source_output['data'], dict): + if field_name in source_output['data']: + result_data[param_name] = source_output['data'][field_name] + else: + result_data[param_name] = None + else: + result_data[param_name] = None + else: + result_data[param_name] = source_output + else: + result_data[param_name] = None + else: + # 格式不正确,使用默认值 + result_data[param_name] = param.get('default_value') + except Exception as e: + logger.warning(f"解析variable_name失败: {variable_name}, 错误: {str(e)}") + result_data[param_name] = param.get('default_value') + else: + # 没有variable_name,使用默认值 + result_data[param_name] = param.get('default_value') + + # 如果没有配置输出参数,返回简化的前一个节点输出(保持向后兼容) + if not output_params: + simplified_outputs = {} + for node_id, output in previous_outputs.items(): + if isinstance(output, dict): + simplified_outputs[node_id] = { + 'success': output.get('success', False), + 'message': output.get('message', ''), + 'data': output.get('data', {}) if not isinstance(output.get('data'), dict) or node_id not in str(output.get('data', {})) else {} + } + else: + simplified_outputs[node_id] = output + result_data = simplified_outputs + + return { + 'success': True, + 'message': '工作流结束', + 'data': result_data + } + + async def _execute_llm_node(self, node: Dict[str, Any], input_data: Dict[str, Any]) -> Dict[str, Any]: + """执行LLM节点""" + config = input_data['node_config'] + + # 获取LLM配置 + model_id = config.get('model_id') + if not model_id: + # 兼容前端的model字段(可能是ID或名称) + model_value = config.get('model_name', config.get('model')) + if model_value: + # 如果是整数,直接作为ID使用 + if isinstance(model_value, int): + model_id = model_value + else: + # 如果是字符串,按名称查询 + llm_config = self.db.query(LLMConfig).filter(LLMConfig.model_name == model_value).first() + if llm_config: + model_id = llm_config.id + + if not model_id: + raise ValueError("未指定有效的大模型配置") + + llm_config = self.db.query(LLMConfig).filter(LLMConfig.id == model_id).first() + if not llm_config: + raise ValueError(f"大模型配置 {model_id} 不存在") + + # 准备提示词 + prompt_template = config.get('prompt', '') + + # 检查是否启用变量替换 + enable_variable_substitution = config.get('enable_variable_substitution', True) + + if enable_variable_substitution: + # 使用增强的变量替换 + prompt = self._substitute_variables(prompt_template, input_data) + else: + prompt = prompt_template + + # 记录处理后的提示词到输入数据中,用于前端显示 + input_data['processed_prompt'] = prompt + input_data['original_prompt'] = prompt_template + + # 调用LLM服务 + try: + response = await self.llm_service.chat_completion( + model_config=llm_config, + messages=[{"role": "user", "content": prompt}], + temperature=config.get('temperature', 0.7), + max_tokens=config.get('max_tokens') + ) + + return { + 'success': True, + 'response': response, + 'prompt': prompt, + 'model': llm_config.model_name, + 'tokens_used': getattr(response, 'usage', {}).get('total_tokens', 0) if hasattr(response, 'usage') else 0 + } + + except Exception as e: + logger.error(f"LLM调用失败: {str(e)}") + raise ValueError(f"LLM调用失败: {str(e)}") + + def _substitute_variables(self, template: str, input_data: Dict[str, Any]) -> str: + """变量替换函数""" + import re + + # 获取解析后的输入参数 + resolved_inputs = input_data.get('resolved_inputs', {}) + + # 获取工作流输入数据 + # input_data['workflow_input'] 包含了用户输入的参数 + workflow_input = input_data.get('workflow_input', {}) + + # 构建变量上下文 + variable_context = {} + + # 首先添加解析后的参数 + variable_context.update(resolved_inputs) + + # 添加工作流输入的顶层字段 + variable_context.update(workflow_input) + + # 如果 workflow_input 包含 user_input 字段,将其内容提升到顶层 + if 'user_input' in workflow_input and isinstance(workflow_input['user_input'], dict): + variable_context.update(workflow_input['user_input']) + + # 添加前一个节点的输出(简化访问) + for node_id, output in input_data.get('previous_outputs', {}).items(): + if isinstance(output, dict): + # 添加节点输出的直接访问 + variable_context[f'node_{node_id}'] = output.get('data', output) + # 如果输出有response字段,也添加直接访问 + if 'response' in output: + variable_context[f'node_{node_id}_response'] = output['response'] + + # 调试日志:打印变量上下文 + logger.info(f"变量替换上下文: {variable_context}") + logger.info(f"原始模板: {template}") + + # 使用正则表达式替换变量 {{variable_name}} 和 {variable_name} + def replace_variable(match): + var_name = match.group(1) + replacement = variable_context.get(var_name, match.group(0)) + logger.info(f"替换变量 {match.group(0)} -> {replacement}") + return str(replacement) + + # 首先替换 {{variable_name}} 格式的变量 + result = re.sub(r'\{\{([^}]+)\}\}', replace_variable, template) + # 然后替换 {variable_name} 格式的变量 + result = re.sub(r'\{([^}]+)\}', replace_variable, result) + + logger.info(f"替换后结果: {result}") + return result + + async def _execute_condition_node(self, node: Dict[str, Any], input_data: Dict[str, Any]) -> Dict[str, Any]: + """执行条件节点""" + config = input_data['node_config'] + condition = config.get('condition', '') + + # 简单的条件评估(生产环境需要更安全的实现) + try: + # 构建评估上下文 + eval_context = { + 'input': input_data['workflow_input'], + 'previous': input_data['previous_outputs'] + } + + # 评估条件 + result = eval(condition, {"__builtins__": {}}, eval_context) + + return { + 'success': True, + 'condition': condition, + 'result': bool(result) + } + + except Exception as e: + logger.error(f"条件评估失败: {str(e)}") + raise ValueError(f"条件评估失败: {str(e)}") + + async def _execute_code_node(self, node: Dict[str, Any], input_data: Dict[str, Any]) -> Dict[str, Any]: + """执行代码节点""" + config = input_data['node_config'] + language = config.get('language', 'python') + code = config.get('code', '') + + if language == 'python': + # 执行Python代码 + execution_result = await self._execute_python_code(code, input_data) + + # 处理输出参数配置 + node_parameters = node.get('parameters', {}) + if node_parameters and 'outputs' in node_parameters: + output_params = node_parameters['outputs'] + code_result = execution_result.get('result', {}) + + # 根据输出参数配置构建最终输出 + final_output = { + 'success': execution_result['success'], + 'code': execution_result['code'], + 'input_parameters': execution_result.get('input_parameters', {}) + } + + # 如果代码返回的是字典,根据输出参数配置提取对应字段 + if isinstance(code_result, dict): + for output_param in output_params: + param_name = output_param.get('name') + if param_name and param_name in code_result: + final_output[param_name] = code_result[param_name] + else: + # 如果代码返回的不是字典,且只有一个输出参数,直接使用返回值 + if len(output_params) == 1: + param_name = output_params[0].get('name') + if param_name: + final_output[param_name] = code_result + + return final_output + else: + # 如果没有输出参数配置,返回原始结果 + return execution_result + else: + raise ValueError(f"不支持的代码语言: {language}") + + async def _execute_python_code(self, code: str, input_data: Dict[str, Any]) -> Dict[str, Any]: + """执行Python代码""" + try: + # 构建执行上下文 + safe_builtins = { + 'len': len, + 'str': str, + 'int': int, + 'float': float, + 'bool': bool, + 'list': list, + 'dict': dict, + 'tuple': tuple, + 'set': set, + 'range': range, + 'enumerate': enumerate, + 'zip': zip, + 'sum': sum, + 'min': min, + 'max': max, + 'abs': abs, + 'round': round, + 'sorted': sorted, + 'reversed': reversed, + 'print': print, + '__import__': __import__, + } + + # 导入常用模块 + import json + import datetime + import math + import re + + exec_context = { + '__builtins__': safe_builtins, + 'json': json, # 允许使用json模块 + 'datetime': datetime, # 允许使用datetime模块 + 'math': math, # 允许使用math模块 + 're': re, # 允许使用re模块 + } + + # 执行代码以定义函数 + exec(code, exec_context) + + # 检查是否定义了main函数 + if 'main' not in exec_context: + raise ValueError("代码中必须定义一个main函数") + + main_function = exec_context['main'] + + # 获取已解析的输入参数 + resolved_inputs = input_data.get('resolved_inputs', {}) + + # 调用main函数并传递参数 + if resolved_inputs: + # 使用解析后的输入参数调用main函数 + result = main_function(**resolved_inputs) + else: + # 如果没有输入参数,直接调用main函数 + result = main_function() + + return { + 'success': True, + 'result': result, + 'code': code, + 'input_parameters': resolved_inputs + } + + except Exception as e: + logger.error(f"Python代码执行失败: {str(e)}") + raise ValueError(f"Python代码执行失败: {str(e)}") + + async def _execute_http_node(self, node: Dict[str, Any], input_data: Dict[str, Any]) -> Dict[str, Any]: + """执行HTTP请求节点""" + import aiohttp + + config = input_data['node_config'] + method = config.get('method', 'GET').upper() + url = config.get('url', '') + headers = config.get('headers', {}) + body = config.get('body') + + try: + async with aiohttp.ClientSession() as session: + async with session.request( + method=method, + url=url, + headers=headers, + data=body + ) as response: + response_text = await response.text() + + return { + 'success': True, + 'status_code': response.status, + 'response': response_text, + 'headers': dict(response.headers) + } + + except Exception as e: + logger.error(f"HTTP请求失败: {str(e)}") + raise ValueError(f"HTTP请求失败: {str(e)}") + + +# 工作流引擎实例 +def get_workflow_engine(db: Session = None) -> WorkflowEngine: + """获取工作流引擎实例""" + if db is None: + db = next(get_db()) + return WorkflowEngine(db) \ No newline at end of file diff --git a/backend/th_agenter/services/zhipu_embeddings.py b/backend/th_agenter/services/zhipu_embeddings.py new file mode 100644 index 0000000..249976f --- /dev/null +++ b/backend/th_agenter/services/zhipu_embeddings.py @@ -0,0 +1,74 @@ +"""Custom ZhipuAI Embeddings using OpenAI compatible API.""" + +import asyncio +from typing import List, Optional +from openai import OpenAI +from langchain_core.embeddings import Embeddings +from ..core.config import settings +from ..utils.logger import get_logger + +logger = get_logger("zhipu_embeddings") + + +class ZhipuOpenAIEmbeddings(Embeddings): + """ZhipuAI Embeddings using OpenAI compatible API.""" + + def __init__( + self, + api_key: Optional[str] = None, + base_url: str = "https://open.bigmodel.cn/api/paas/v4", + model: str = "embedding-3", + dimensions: int = 1024 + ): + self.api_key = api_key or settings.embedding.zhipu_api_key + self.base_url = base_url + self.model = model + self.dimensions = dimensions + + self.client = OpenAI( + api_key=self.api_key, + base_url=self.base_url + ) + + logger.info(f"ZhipuOpenAI Embeddings initialized with model: {self.model}") + + def embed_documents(self, texts: List[str]) -> List[List[float]]: + """Embed search docs.""" + try: + embeddings = [] + for text in texts: + response = self.client.embeddings.create( + model=self.model, + input=text, + dimensions=self.dimensions, + encoding_format="float" + ) + embeddings.append(response.data[0].embedding) + return embeddings + except Exception as e: + logger.error(f"Error embedding documents: {e}") + raise + + def embed_query(self, text: str) -> List[float]: + """Embed query text.""" + try: + response = self.client.embeddings.create( + model=self.model, + input=text, + dimensions=self.dimensions, + encoding_format="float" + ) + return response.data[0].embedding + except Exception as e: + logger.error(f"Error embedding query: {e}") + raise + + async def aembed_documents(self, texts: List[str]) -> List[List[float]]: + """Async embed search docs.""" + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, self.embed_documents, texts) + + async def aembed_query(self, text: str) -> List[float]: + """Async embed query text.""" + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, self.embed_query, text) \ No newline at end of file diff --git a/backend/th_agenter/utils/__init__.py b/backend/th_agenter/utils/__init__.py new file mode 100644 index 0000000..0f68c12 --- /dev/null +++ b/backend/th_agenter/utils/__init__.py @@ -0,0 +1,30 @@ +"""Utilities package for the chat agent application.""" + +from .logger import get_logger, setup_logger +from .exceptions import ( + ChatAgentException, + ValidationError, + AuthenticationError, + AuthorizationError, + NotFoundError, + ConversationNotFoundError, + UserNotFoundError, + ChatServiceError, + OpenAIError, + DatabaseError +) + +__all__ = [ + "get_logger", + "setup_logger", + "ChatAgentException", + "ValidationError", + "AuthenticationError", + "AuthorizationError", + "NotFoundError", + "ConversationNotFoundError", + "UserNotFoundError", + "ChatServiceError", + "OpenAIError", + "DatabaseError" +] \ No newline at end of file diff --git a/backend/th_agenter/utils/__pycache__/__init__.cpython-313.pyc b/backend/th_agenter/utils/__pycache__/__init__.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39a423dd9d22b8a51777d79d657a2336bbcb6f50 GIT binary patch literal 698 zcmb7>&ui2`6vrpYZnB#n+ge4?Q#=N=(vu(}EbR(Tp(>%C7KSt#vx8|Syi7{ln^*7t z5&lWeRYbgrd+619Nf)g*-{CWS-@NyoA8#E+J~0s-d|Q3A3Hgd*6Gr#3er=XdBqcyn zE42?TYJ*K3aAMTX;fRi5OkHrP2Ogckgic{feeh`j0SzIf5kzzbvzR<4>FA9GTje-$ zFZYg>sDu){jhifgpOrj*Cm~iV9_On}#U@c3Hcch+ObJ=<{;oE)~(}~!_Cw-UM1ylCUQRX2d^4l&tDEAI>=OZlC{HL5`tr12P4E7 z;UYZ51TjV6v>-r)hzKz=I7+s)$5zx?Pl6ZOCH#LSb|MW1H<9G9ILcDrOu)Iu?~+g#|$vr}1g6@LzQjVq3}ePHm@ du`KHgdGeh+ys`t!z9Jj^x*rAh2M_L;s^32(#uWen literal 0 HcmV?d00001 diff --git a/backend/th_agenter/utils/__pycache__/exceptions.cpython-313.pyc b/backend/th_agenter/utils/__pycache__/exceptions.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9b73bbed14288b63e3941a40d898f45617cc3ace GIT binary patch literal 8476 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 Z|IYaSlNrh}L*JNHqoM071|gO4{{bb^zefN7 literal 0 HcmV?d00001 diff --git a/backend/th_agenter/utils/__pycache__/logger.cpython-313.pyc b/backend/th_agenter/utils/__pycache__/logger.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..01bc75c53e51f6db6aa32aa7e393655e3fda52ec GIT binary patch literal 2397 zcmbVN&2JM&6rZ(s*Xwnh#C#;A5O)$V#NdUNAgOk-5Q-aXu{ZXDz3a}d z+mc?w39257)K*QUN=@5CL8{aq`)A-JlHF~Qkl@mrX`tu6*$*~Ety*;~&wKOc&HI>n zzqecQxQJj}{o|PS1&`2Uy6{KTW468p=5r(?f@DSxU0?{~&=3g$4POY)vV_HSkG;Un z@`T4I>HOrF%w;1o5C2FOW)AGgLY9#_eHQ^873AnE=sJ6yA<=BKAB}liM09NY(? znz7lp6{`ePn6J`vO4(+0rJ~wgS$$6}+d|nW=9aXwYV$<3YDCY6gZNPdc)H#?6QZ}Z z@M24X_0G|cGH8r(rFu9Mme`#d2ual9j^W=5MsUV|pKUq|F@F*G&oENlmj=|gH0DL; zM}xOV-X7@tT)$HTa`;q8X9FL9_oDr1VV8eO+T(Kp^*!obhLgGjxzA--U&G0^$Cn2* z!)?*#!jmk#*yfJ(0hywAxj{J2x0mu>_5y8(rFe_ zk^h-R#E+2Xc?hM2sh5qiDA+R>tFxp!p?CAi zf(d&L`XivFSQfFlTD7VYJ33{M3J8FW%N9|U%6a#!9X4;8Hg8%5cp1A53LTX_?lvj<710)_=3D_e1h#22G9SaVf}SN&``$)8xgJlhS{w00D^o2Vak1O;x8@scPo3@AWcR<2 zd^Y{n^jhZog>M$VduL?F z{G{Wfj)u@%7kbwUKV5!6x$!4pwEO6K_tASNe=gR$r|z^hg*^?SuP*ehr8kA4rqJFH zdg?;Y>d>ZeWmeU^wxj`)C@%g3-d{$dTj4l>Uo3dvk#vaAUQaq;N;6K~nKVC44#I4UQ2YL4 z9|6H>_?xtq!ej43ZMz$>1M9H^s~_GQy?6LQ?`s>e;}6*5Bn1lTuq_6DY|iO6bLloa zlFJp0d@kqurR|>r*D95@YaTMJn^iz3p;rr#0^sp(3QoC^B!~%()0>(?te}T^}b`_!b{!PTZAS2*~P{uNS{ud`?*> Gq`v@oVkM6N literal 0 HcmV?d00001 diff --git a/backend/th_agenter/utils/__pycache__/schemas.cpython-313.pyc b/backend/th_agenter/utils/__pycache__/schemas.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2a33bddfe48b65445874b8cae4ad58d73056cd33 GIT binary patch literal 19324 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 JSONResponse: + """Handle ChatAgentException and its subclasses.""" + logger.error( + f"ChatAgentException: {exc.message}", + extra={ + "status_code": exc.status_code, + "details": exc.details, + "path": request.url.path, + "method": request.method + } + ) + + return JSONResponse( + status_code=exc.status_code, + content={ + "error": { + "message": exc.message, + "type": exc.__class__.__name__, + "details": exc.details + } + } + ) + + +async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse: + """Handle HTTPException.""" + logger.warning( + f"HTTPException: {exc.detail}", + extra={ + "status_code": exc.status_code, + "path": request.url.path, + "method": request.method + } + ) + + return JSONResponse( + status_code=exc.status_code, + content={ + "error": { + "message": exc.detail, + "type": "HTTPException" + } + } + ) + + +async def general_exception_handler(request: Request, exc: Exception) -> JSONResponse: + """Handle general exceptions.""" + logger.error( + f"Unhandled exception: {str(exc)}", + extra={ + "exception_type": exc.__class__.__name__, + "path": request.url.path, + "method": request.method + }, + exc_info=True + ) + + return JSONResponse( + status_code=HTTP_500_INTERNAL_SERVER_ERROR, + content={ + "error": { + "message": "Internal server error", + "type": "InternalServerError" + } + } + ) \ No newline at end of file diff --git a/backend/th_agenter/utils/file_utils.py b/backend/th_agenter/utils/file_utils.py new file mode 100644 index 0000000..79b36ce --- /dev/null +++ b/backend/th_agenter/utils/file_utils.py @@ -0,0 +1,191 @@ +"""File utilities.""" + +import os +import re +import hashlib +import mimetypes +from pathlib import Path +from typing import Optional, List, Dict, Any + + +class FileUtils: + """Utility class for file operations.""" + + # Allowed file extensions for document upload + ALLOWED_EXTENSIONS = { + '.txt', '.md', '.csv', # Text files + '.pdf', # PDF files + '.docx', '.doc', # Word documents + '.xlsx', '.xls', # Excel files + '.pptx', '.ppt', # PowerPoint files + '.rtf', # Rich text format + '.odt', '.ods', '.odp' # OpenDocument formats + } + + # MIME type mappings + MIME_TYPE_MAPPING = { + '.txt': 'text/plain', + '.md': 'text/markdown', + '.csv': 'text/csv', + '.pdf': 'application/pdf', + '.docx': 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + '.doc': 'application/msword', + '.xlsx': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', + '.xls': 'application/vnd.ms-excel', + '.pptx': 'application/vnd.openxmlformats-officedocument.presentationml.presentation', + '.ppt': 'application/vnd.ms-powerpoint', + '.rtf': 'application/rtf', + '.odt': 'application/vnd.oasis.opendocument.text', + '.ods': 'application/vnd.oasis.opendocument.spreadsheet', + '.odp': 'application/vnd.oasis.opendocument.presentation' + } + + @staticmethod + def sanitize_filename(filename: str) -> str: + """Sanitize filename to remove dangerous characters.""" + # Remove or replace dangerous characters + filename = re.sub(r'[<>:"/\\|?*]', '_', filename) + + # Remove leading/trailing spaces and dots + filename = filename.strip(' .') + + # Ensure filename is not empty + if not filename: + filename = 'unnamed_file' + + # Limit filename length + if len(filename) > 255: + name, ext = os.path.splitext(filename) + filename = name[:255-len(ext)] + ext + + return filename + + @staticmethod + def get_file_hash(file_path: str, algorithm: str = 'md5') -> str: + """Calculate file hash.""" + hash_func = hashlib.new(algorithm) + + with open(file_path, 'rb') as f: + for chunk in iter(lambda: f.read(4096), b""): + hash_func.update(chunk) + + return hash_func.hexdigest() + + @staticmethod + def get_file_info(file_path: str) -> Dict[str, Any]: + """Get comprehensive file information.""" + path = Path(file_path) + + if not path.exists(): + raise FileNotFoundError(f"File not found: {file_path}") + + stat = path.stat() + + # Get MIME type + mime_type, encoding = mimetypes.guess_type(str(path)) + + return { + 'filename': path.name, + 'extension': path.suffix.lower(), + 'size_bytes': stat.st_size, + 'size_mb': round(stat.st_size / (1024 * 1024), 2), + 'mime_type': mime_type, + 'encoding': encoding, + 'created_at': stat.st_ctime, + 'modified_at': stat.st_mtime, + 'is_file': path.is_file(), + 'is_readable': os.access(path, os.R_OK) + } + + @staticmethod + def validate_file_extension(filename: str, allowed_extensions: Optional[List[str]] = None) -> bool: + """Validate file extension.""" + if allowed_extensions is None: + allowed_extensions = list(FileUtils.ALLOWED_EXTENSIONS) + + extension = Path(filename).suffix.lower() + return extension in allowed_extensions + + @staticmethod + def validate_file_size(file_size: int, max_size: int) -> bool: + """Validate file size.""" + return file_size <= max_size + + @staticmethod + def create_directory(directory_path: str) -> bool: + """Create directory if it doesn't exist.""" + try: + Path(directory_path).mkdir(parents=True, exist_ok=True) + return True + except Exception: + return False + + @staticmethod + def delete_file(file_path: str) -> bool: + """Safely delete a file.""" + try: + path = Path(file_path) + if path.exists() and path.is_file(): + path.unlink() + return True + return False + except Exception: + return False + + @staticmethod + def get_mime_type(filename: str) -> Optional[str]: + """Get MIME type for filename.""" + extension = Path(filename).suffix.lower() + return FileUtils.MIME_TYPE_MAPPING.get(extension) + + @staticmethod + def format_file_size(size_bytes: int) -> str: + """Format file size in human readable format.""" + if size_bytes == 0: + return "0 B" + + size_names = ["B", "KB", "MB", "GB", "TB"] + i = 0 + size = float(size_bytes) + + while size >= 1024.0 and i < len(size_names) - 1: + size /= 1024.0 + i += 1 + + return f"{size:.1f} {size_names[i]}" + + @staticmethod + def is_text_file(filename: str) -> bool: + """Check if file is a text file.""" + extension = Path(filename).suffix.lower() + return extension in {'.txt', '.md', '.csv', '.rtf'} + + @staticmethod + def is_pdf_file(filename: str) -> bool: + """Check if file is a PDF.""" + extension = Path(filename).suffix.lower() + return extension == '.pdf' + + @staticmethod + def is_office_file(filename: str) -> bool: + """Check if file is an Office document.""" + extension = Path(filename).suffix.lower() + return extension in {'.docx', '.doc', '.xlsx', '.xls', '.pptx', '.ppt', '.odt', '.ods', '.odp'} + + @staticmethod + def get_file_category(filename: str) -> str: + """Get file category based on extension.""" + extension = Path(filename).suffix.lower() + + if extension in {'.txt', '.md', '.csv', '.rtf'}: + return 'text' + elif extension == '.pdf': + return 'pdf' + elif extension in {'.docx', '.doc', '.odt'}: + return 'document' + elif extension in {'.xlsx', '.xls', '.ods'}: + return 'spreadsheet' + elif extension in {'.pptx', '.ppt', '.odp'}: + return 'presentation' + else: + return 'unknown' \ No newline at end of file diff --git a/backend/th_agenter/utils/logger.py b/backend/th_agenter/utils/logger.py new file mode 100644 index 0000000..92c0b54 --- /dev/null +++ b/backend/th_agenter/utils/logger.py @@ -0,0 +1,68 @@ +"""Logging configuration for the chat agent application.""" + +import logging +import sys +from pathlib import Path +from typing import Optional + + +def setup_logger( + name: str = "th_agenter", + level: str = "DEBUG", + log_file: Optional[str] = None +) -> logging.Logger: + """Setup logger with console and optional file output. + + Args: + name: Logger name + level: Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL) + log_file: Optional log file path + + Returns: + Configured logger instance + """ + logger = logging.getLogger(name) + + # Clear existing handlers + logger.handlers.clear() + + # Set level + logger.setLevel(getattr(logging, level.upper())) + + # Create formatter + formatter = logging.Formatter( + "%(asctime)s - %(name)s - %(levelname)s - %(message)s", + datefmt="%Y-%m-%d %H:%M:%S" + ) + + # Console handler + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + + # File handler (optional) + if log_file: + log_path = Path(log_file) + log_path.parent.mkdir(parents=True, exist_ok=True) + + file_handler = logging.FileHandler(log_path, encoding="utf-8") + file_handler.setFormatter(formatter) + logger.addHandler(file_handler) + + return logger + + +# Default logger instance +logger = setup_logger() + + +def get_logger(name: str) -> logging.Logger: + """Get a logger instance with the specified name. + + Args: + name: Logger name + + Returns: + Logger instance + """ + return logging.getLogger(f"th_agenter.{name}") \ No newline at end of file diff --git a/backend/th_agenter/utils/node_parameters.py b/backend/th_agenter/utils/node_parameters.py new file mode 100644 index 0000000..3d950cd --- /dev/null +++ b/backend/th_agenter/utils/node_parameters.py @@ -0,0 +1,217 @@ +""" +节点参数默认配置工具 +""" +from typing import Dict, List +from ..schemas.workflow import NodeInputOutput, NodeParameter, ParameterType, NodeType + + +def get_default_node_parameters(node_type: NodeType) -> NodeInputOutput: + """获取节点类型的默认输入输出参数""" + + if node_type == NodeType.START: + return NodeInputOutput( + inputs=[ + NodeParameter( + name="workflow_input", + type=ParameterType.OBJECT, + description="工作流初始输入数据", + required=False, + source="input" + ) + ], + outputs=[ + NodeParameter( + name="data", + type=ParameterType.OBJECT, + description="开始节点输出数据" + ) + ] + ) + + elif node_type == NodeType.END: + return NodeInputOutput( + inputs=[ + NodeParameter( + name="final_result", + type=ParameterType.OBJECT, + description="最终结果数据", + required=False, + source="node" + ) + ], + outputs=[ + NodeParameter( + name="workflow_result", + type=ParameterType.OBJECT, + description="工作流最终输出" + ) + ] + ) + + elif node_type == NodeType.LLM: + return NodeInputOutput( + inputs=[ + NodeParameter( + name="prompt_variables", + type=ParameterType.OBJECT, + description="Prompt中使用的变量", + required=False, + source="node" + ), + NodeParameter( + name="user_input", + type=ParameterType.STRING, + description="用户输入文本", + required=False, + source="input" + ) + ], + outputs=[ + NodeParameter( + name="response", + type=ParameterType.STRING, + description="LLM生成的回复" + ), + NodeParameter( + name="tokens_used", + type=ParameterType.NUMBER, + description="使用的token数量" + ) + ] + ) + + elif node_type == NodeType.CODE: + return NodeInputOutput( + inputs=[ + NodeParameter( + name="input_data", + type=ParameterType.OBJECT, + description="代码执行的输入数据", + required=False, + source="node" + ) + ], + outputs=[ + NodeParameter( + name="result", + type=ParameterType.OBJECT, + description="代码执行结果" + ), + NodeParameter( + name="output", + type=ParameterType.STRING, + description="代码输出内容" + ) + ] + ) + + elif node_type == NodeType.HTTP: + return NodeInputOutput( + inputs=[ + NodeParameter( + name="url_params", + type=ParameterType.OBJECT, + description="URL参数", + required=False, + source="node" + ), + NodeParameter( + name="request_body", + type=ParameterType.OBJECT, + description="请求体数据", + required=False, + source="node" + ) + ], + outputs=[ + NodeParameter( + name="response_data", + type=ParameterType.OBJECT, + description="HTTP响应数据" + ), + NodeParameter( + name="status_code", + type=ParameterType.NUMBER, + description="HTTP状态码" + ) + ] + ) + + elif node_type == NodeType.CONDITION: + return NodeInputOutput( + inputs=[ + NodeParameter( + name="condition_data", + type=ParameterType.OBJECT, + description="条件判断的输入数据", + required=True, + source="node" + ) + ], + outputs=[ + NodeParameter( + name="result", + type=ParameterType.BOOLEAN, + description="条件判断结果" + ), + NodeParameter( + name="branch", + type=ParameterType.STRING, + description="执行分支(true/false)" + ) + ] + ) + + else: + # 默认参数 + return NodeInputOutput( + inputs=[ + NodeParameter( + name="input", + type=ParameterType.OBJECT, + description="节点输入数据", + required=False, + source="node" + ) + ], + outputs=[ + NodeParameter( + name="output", + type=ParameterType.OBJECT, + description="节点输出数据" + ) + ] + ) + + +def validate_parameter_connections(nodes: List[Dict], connections: List[Dict]) -> List[str]: + """验证节点参数连接的有效性""" + errors = [] + node_dict = {node['id']: node for node in nodes} + + for node in nodes: + if 'parameters' not in node or not node['parameters']: + continue + + for input_param in node['parameters'].get('inputs', []): + if input_param.get('source') == 'node': + source_node_id = input_param.get('source_node_id') + source_field = input_param.get('source_field') + + if not source_node_id: + errors.append(f"节点 {node['name']} 的输入参数 {input_param['name']} 缺少来源节点ID") + continue + + if source_node_id not in node_dict: + errors.append(f"节点 {node['name']} 的输入参数 {input_param['name']} 引用了不存在的节点 {source_node_id}") + continue + + source_node = node_dict[source_node_id] + if 'parameters' in source_node and source_node['parameters']: + source_outputs = source_node['parameters'].get('outputs', []) + output_fields = [output['name'] for output in source_outputs] + + if source_field and source_field not in output_fields: + errors.append(f"节点 {node['name']} 的输入参数 {input_param['name']} 引用了节点 {source_node['name']} 不存在的输出字段 {source_field}") + + return errors \ No newline at end of file diff --git a/backend/th_agenter/utils/schemas.py b/backend/th_agenter/utils/schemas.py new file mode 100644 index 0000000..2edf80a --- /dev/null +++ b/backend/th_agenter/utils/schemas.py @@ -0,0 +1,375 @@ +"""Pydantic schemas for API requests and responses.""" + +from typing import Optional, List, Any, Dict, TYPE_CHECKING +from datetime import datetime +from pydantic import BaseModel, Field +from enum import Enum + +if TYPE_CHECKING: + from ..schemas.permission import RoleResponse + + +class MessageRole(str, Enum): + """Message role enumeration.""" + USER = "user" + ASSISTANT = "assistant" + SYSTEM = "system" + + +class MessageType(str, Enum): + """Message type enumeration.""" + TEXT = "text" + IMAGE = "image" + FILE = "file" + AUDIO = "audio" + + +# Base schemas +class BaseResponse(BaseModel): + """Base response schema.""" + id: int + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + + +# User schemas +class UserBase(BaseModel): + """User base schema.""" + username: str = Field(..., min_length=3, max_length=50) + email: str = Field(..., max_length=100) + full_name: Optional[str] = Field(None, max_length=100) + bio: Optional[str] = None + avatar_url: Optional[str] = None + + +class UserCreate(UserBase): + """User creation schema.""" + password: str = Field(..., min_length=6) + + +class UserUpdate(BaseModel): + """User update schema.""" + username: Optional[str] = Field(None, min_length=3, max_length=50) + email: Optional[str] = Field(None, max_length=100) + full_name: Optional[str] = Field(None, max_length=100) + bio: Optional[str] = None + avatar_url: Optional[str] = None + password: Optional[str] = Field(None, min_length=6) + is_active: Optional[bool] = None + department_id: Optional[int] = None + + +class UserResponse(BaseResponse, UserBase): + """User response schema.""" + is_active: bool + department_id: Optional[int] = None + roles: Optional[List['RoleResponse']] = Field(default=[], description="用户角色列表") + permissions: Optional[List[Dict[str, Any]]] = Field(default=[], description="用户权限列表") + is_superuser: Optional[bool] = Field(default=False, description="是否为超级管理员") + + @classmethod + def from_orm(cls, obj): + """从ORM对象创建响应对象,安全处理关系属性.""" + # 获取基本字段 + data = { + 'id': obj.id, + 'username': obj.username, + 'email': obj.email, + 'full_name': obj.full_name, + 'is_active': obj.is_active, + 'department_id': obj.department_id, + 'created_at': obj.created_at, + 'updated_at': obj.updated_at, + 'created_by': obj.created_by, + 'updated_by': obj.updated_by, + } + + # 安全处理roles关系 + try: + # 尝试访问roles,如果成功则包含,否则使用空列表 + if hasattr(obj, 'roles') and obj.roles is not None: + from ..schemas.permission import RoleResponse + data['roles'] = [RoleResponse.from_orm(role) for role in obj.roles] + else: + data['roles'] = [] + except Exception: + # 如果访问roles失败(DetachedInstanceError),使用空列表 + data['roles'] = [] + + # 添加权限信息 + try: + # 获取数据库会话 + from sqlalchemy.orm import object_session + session = object_session(obj) + + if obj.has_role('SUPER_ADMIN'): + # 超级管理员拥有所有权限 + if session: + from ..models.permission import Permission + all_permissions = session.query(Permission).filter(Permission.is_active == True).all() + data['permissions'] = [{'code': perm.code, 'name': perm.name} for perm in all_permissions] + else: + data['permissions'] = [{'code': '*', 'name': '所有权限'}] + else: + # 从角色获取权限 + permissions = set() + for role in obj.roles: + if role.is_active: + for perm in role.permissions: + if perm.is_active: + permissions.add((perm.code, perm.name)) + data['permissions'] = [{'code': code, 'name': name} for code, name in permissions] + except Exception as e: + data['permissions'] = [] + + # 添加超级管理员状态 + try: + data['is_superuser'] = obj.is_superuser() + except Exception: + data['is_superuser'] = False + + return cls(**data) + + +# Authentication schemas +class LoginRequest(BaseModel): + """Login request schema.""" + email: str = Field(..., max_length=100) + password: str = Field(..., min_length=6) + + +class Token(BaseModel): + """Token response schema.""" + access_token: str + token_type: str + expires_in: int + + +# Conversation schemas +class ConversationBase(BaseModel): + """Conversation base schema.""" + title: str = Field(..., min_length=1, max_length=200) + system_prompt: Optional[str] = None + model_name: str = Field(default="gpt-3.5-turbo", max_length=100) + temperature: str = Field(default="0.7", max_length=10) + max_tokens: int = Field(default=2048, ge=1, le=8192) + knowledge_base_id: Optional[int] = None + + +class ConversationCreate(ConversationBase): + """Conversation creation schema.""" + pass + + +class ConversationUpdate(BaseModel): + """Conversation update schema.""" + title: Optional[str] = Field(None, min_length=1, max_length=200) + system_prompt: Optional[str] = None + model_name: Optional[str] = Field(None, max_length=100) + temperature: Optional[str] = Field(None, max_length=10) + max_tokens: Optional[int] = Field(None, ge=1, le=8192) + is_archived: Optional[bool] = None + + +class ConversationResponse(BaseResponse, ConversationBase): + """Conversation response schema.""" + user_id: int + is_archived: bool + message_count: int = 0 + last_message_at: Optional[datetime] = None + + +# Message schemas +class MessageBase(BaseModel): + """Message base schema.""" + content: str = Field(..., min_length=1) + role: MessageRole + message_type: MessageType = MessageType.TEXT + metadata: Optional[Dict[str, Any]] = Field(None, alias="message_metadata") + + +class MessageCreate(MessageBase): + """Message creation schema.""" + conversation_id: int + + +class MessageResponse(BaseResponse, MessageBase): + """Message response schema.""" + conversation_id: int + context_documents: Optional[List[Dict[str, Any]]] = None + prompt_tokens: Optional[int] = None + completion_tokens: Optional[int] = None + total_tokens: Optional[int] = None + + class Config: + from_attributes = True + populate_by_name = True + + +# Chat schemas +class ChatRequest(BaseModel): + """Chat request schema.""" + message: str = Field(..., min_length=1, max_length=10000) + stream: bool = Field(default=False) + use_knowledge_base: bool = Field(default=False) + knowledge_base_id: Optional[int] = Field(None, description="Knowledge base ID for RAG mode") + use_agent: bool = Field(default=False, description="Enable agent mode with tool calling capabilities") + use_langgraph: bool = Field(default=False, description="Enable LangGraph agent mode with advanced tool calling") + temperature: Optional[float] = Field(None, ge=0.0, le=2.0) + max_tokens: Optional[int] = Field(None, ge=1, le=8192) + + +class ChatResponse(BaseModel): + """Chat response schema.""" + user_message: MessageResponse + assistant_message: MessageResponse + total_tokens: Optional[int] = None + model_used: str + + +class StreamChunk(BaseModel): + """Stream chunk schema.""" + content: str + role: MessageRole = MessageRole.ASSISTANT + finish_reason: Optional[str] = None + tokens_used: Optional[int] = None + + +# Knowledge Base schemas +class KnowledgeBaseBase(BaseModel): + """Knowledge base base schema.""" + name: str = Field(..., min_length=1, max_length=100) + description: Optional[str] = None + embedding_model: str = Field(default="sentence-transformers/all-MiniLM-L6-v2") + chunk_size: int = Field(default=1000, ge=100, le=5000) + chunk_overlap: int = Field(default=200, ge=0, le=1000) + + +class KnowledgeBaseCreate(KnowledgeBaseBase): + """Knowledge base creation schema.""" + pass + + +class KnowledgeBaseUpdate(BaseModel): + """Knowledge base update schema.""" + name: Optional[str] = Field(None, min_length=1, max_length=100) + description: Optional[str] = None + embedding_model: Optional[str] = None + chunk_size: Optional[int] = Field(None, ge=100, le=5000) + chunk_overlap: Optional[int] = Field(None, ge=0, le=1000) + is_active: Optional[bool] = None + + +class KnowledgeBaseResponse(BaseResponse, KnowledgeBaseBase): + """Knowledge base response schema.""" + is_active: bool + vector_db_type: str + collection_name: Optional[str] + document_count: int = 0 + active_document_count: int = 0 + + +# Document schemas +class DocumentBase(BaseModel): + """Document base schema.""" + filename: str + original_filename: str + file_type: str + file_size: int + + +class DocumentUpload(BaseModel): + """Document upload schema.""" + knowledge_base_id: int + process_immediately: bool = Field(default=True) + + +class DocumentResponse(BaseResponse, DocumentBase): + """Document response schema.""" + knowledge_base_id: int + file_path: str + mime_type: Optional[str] + is_processed: bool + processing_error: Optional[str] + chunk_count: int = 0 + embedding_model: Optional[str] + file_size_mb: float + + +class DocumentListResponse(BaseModel): + """Document list response schema.""" + documents: List[DocumentResponse] + total: int + page: int + page_size: int + + +class DocumentProcessingStatus(BaseModel): + """Document processing status schema.""" + document_id: int + status: str # 'pending', 'processing', 'completed', 'failed' + progress: float = Field(default=0.0, ge=0.0, le=100.0) + error_message: Optional[str] = None + chunks_created: int = 0 + estimated_time_remaining: Optional[int] = None # seconds + + +# Error schemas +# Document chunk schemas +class DocumentChunk(BaseModel): + """Document chunk schema.""" + id: str + content: str + metadata: Dict[str, Any] = Field(default_factory=dict) + page_number: Optional[int] = None + chunk_index: int + start_char: Optional[int] = None + end_char: Optional[int] = None + + +class DocumentChunksResponse(BaseModel): + """Document chunks response schema.""" + document_id: int + document_name: str + total_chunks: int + chunks: List[DocumentChunk] + + +class ErrorResponse(BaseModel): + """Error response schema.""" + error: str + detail: Optional[str] = None + code: Optional[str] = None + +# 通用返回结构 +class NormalResponse(BaseModel): + success: bool + message: str + +class ExcelPreviewRequest(BaseModel): + file_id: str + page: int = 1 + page_size: int = 20 + +class FileListResponse(BaseModel): + success: bool + message: str + data: Optional[Dict[str, Any]] = None + + +# 解决前向引用问题 +def rebuild_models(): + """重建模型以解决前向引用问题.""" + try: + from ..schemas.permission import RoleResponse + UserResponse.model_rebuild() + except ImportError: + # 如果无法导入RoleResponse,跳过重建 + pass + + +# 在模块加载时尝试重建模型 +rebuild_models() \ No newline at end of file diff --git a/frontend/.env b/frontend/.env new file mode 100644 index 0000000..6c46038 --- /dev/null +++ b/frontend/.env @@ -0,0 +1,19 @@ +# ======================================== +# 前端环境变量配置示例 +# ======================================== +# 注意:实际的环境变量配置现在统一在 backend/.env 文件中 +# 这个文件仅作为参考,实际部署时需要将这些变量设置到运行环境中 + +# API Configuration +VITE_API_BASE_URL=http://localhost:8000/api + +# App Configuration +VITE_APP_TITLE=TH-Agenter +VITE_APP_VERSION=1.0.0 + +# Development Configuration +VITE_ENABLE_MOCK=false + +# File Upload Configuration +VITE_UPLOAD_MAX_SIZE=10485760 +VITE_SUPPORTED_FILE_TYPES=pdf,txt,md,doc,docx,ppt,pptx,xls,xlsx \ No newline at end of file diff --git a/frontend/.env.development b/frontend/.env.development new file mode 100644 index 0000000..2b9066c --- /dev/null +++ b/frontend/.env.development @@ -0,0 +1,16 @@ +# API Configuration +# VITE_API_BASE_URL=http://localhost:3000/api +# 开发环境变量 +VITE_API_BASE_URL=/api +VITE_API_TARGET=http://localhost:8001 # 本地后端地址 + +# App Configuration +VITE_APP_TITLE=TH-Agenter +VITE_APP_VERSION=1.0.0 + +# Development Configuration +VITE_ENABLE_MOCK=false + +# File Upload Configuration +VITE_UPLOAD_MAX_SIZE=10485760 +VITE_SUPPORTED_FILE_TYPES=pdf,txt,md,doc,docx,ppt,pptx,xls,xlsx \ No newline at end of file diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..56f02f8 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,19 @@ +# ======================================== +# 前端环境变量配置示例 +# ======================================== +# 注意:实际的环境变量配置现在统一在 backend/.env 文件中 +# 这个文件仅作为参考,实际部署时需要将这些变量设置到运行环境中 + +# API Configuration +VITE_API_BASE_URL=http://localhost:8000/api + +# App Configuration +VITE_APP_TITLE=TH-Agenter +VITE_APP_VERSION=1.0.0 + +# Development Configuration +VITE_ENABLE_MOCK=false + +# File Upload Configuration +VITE_UPLOAD_MAX_SIZE=10485760 +VITE_SUPPORTED_FILE_TYPES=pdf,txt,md,doc,docx,ppt,pptx,xls,xlsx \ No newline at end of file diff --git a/frontend/auto-imports.d.ts b/frontend/auto-imports.d.ts new file mode 100644 index 0000000..23ae575 --- /dev/null +++ b/frontend/auto-imports.d.ts @@ -0,0 +1,89 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// noinspection JSUnusedGlobalSymbols +// Generated by unplugin-auto-import +export {} +declare global { + const EffectScope: typeof import('vue')['EffectScope'] + const ElMessage: typeof import('element-plus/es')['ElMessage'] + const acceptHMRUpdate: typeof import('pinia')['acceptHMRUpdate'] + const axios: typeof import('axios')['default'] + const computed: typeof import('vue')['computed'] + const createApp: typeof import('vue')['createApp'] + const createPinia: typeof import('pinia')['createPinia'] + const customRef: typeof import('vue')['customRef'] + const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] + const defineComponent: typeof import('vue')['defineComponent'] + const defineStore: typeof import('pinia')['defineStore'] + const effectScope: typeof import('vue')['effectScope'] + const getActivePinia: typeof import('pinia')['getActivePinia'] + const getCurrentInstance: typeof import('vue')['getCurrentInstance'] + const getCurrentScope: typeof import('vue')['getCurrentScope'] + const h: typeof import('vue')['h'] + const inject: typeof import('vue')['inject'] + const isProxy: typeof import('vue')['isProxy'] + const isReactive: typeof import('vue')['isReactive'] + const isReadonly: typeof import('vue')['isReadonly'] + const isRef: typeof import('vue')['isRef'] + const mapActions: typeof import('pinia')['mapActions'] + const mapGetters: typeof import('pinia')['mapGetters'] + const mapState: typeof import('pinia')['mapState'] + const mapStores: typeof import('pinia')['mapStores'] + const mapWritableState: typeof import('pinia')['mapWritableState'] + const markRaw: typeof import('vue')['markRaw'] + const nextTick: typeof import('vue')['nextTick'] + const onActivated: typeof import('vue')['onActivated'] + const onBeforeMount: typeof import('vue')['onBeforeMount'] + const onBeforeRouteLeave: typeof import('vue-router')['onBeforeRouteLeave'] + const onBeforeRouteUpdate: typeof import('vue-router')['onBeforeRouteUpdate'] + const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] + const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] + const onDeactivated: typeof import('vue')['onDeactivated'] + const onErrorCaptured: typeof import('vue')['onErrorCaptured'] + const onMounted: typeof import('vue')['onMounted'] + const onRenderTracked: typeof import('vue')['onRenderTracked'] + const onRenderTriggered: typeof import('vue')['onRenderTriggered'] + const onScopeDispose: typeof import('vue')['onScopeDispose'] + const onServerPrefetch: typeof import('vue')['onServerPrefetch'] + const onUnmounted: typeof import('vue')['onUnmounted'] + const onUpdated: typeof import('vue')['onUpdated'] + const onWatcherCleanup: typeof import('vue')['onWatcherCleanup'] + const provide: typeof import('vue')['provide'] + const reactive: typeof import('vue')['reactive'] + const readonly: typeof import('vue')['readonly'] + const ref: typeof import('vue')['ref'] + const resolveComponent: typeof import('vue')['resolveComponent'] + const setActivePinia: typeof import('pinia')['setActivePinia'] + const setMapStoreSuffix: typeof import('pinia')['setMapStoreSuffix'] + const shallowReactive: typeof import('vue')['shallowReactive'] + const shallowReadonly: typeof import('vue')['shallowReadonly'] + const shallowRef: typeof import('vue')['shallowRef'] + const storeToRefs: typeof import('pinia')['storeToRefs'] + const toRaw: typeof import('vue')['toRaw'] + const toRef: typeof import('vue')['toRef'] + const toRefs: typeof import('vue')['toRefs'] + const toValue: typeof import('vue')['toValue'] + const triggerRef: typeof import('vue')['triggerRef'] + const unref: typeof import('vue')['unref'] + const useAttrs: typeof import('vue')['useAttrs'] + const useCssModule: typeof import('vue')['useCssModule'] + const useCssVars: typeof import('vue')['useCssVars'] + const useId: typeof import('vue')['useId'] + const useLink: typeof import('vue-router')['useLink'] + const useModel: typeof import('vue')['useModel'] + const useRoute: typeof import('vue-router')['useRoute'] + const useRouter: typeof import('vue-router')['useRouter'] + const useSlots: typeof import('vue')['useSlots'] + const useTemplateRef: typeof import('vue')['useTemplateRef'] + const watch: typeof import('vue')['watch'] + const watchEffect: typeof import('vue')['watchEffect'] + const watchPostEffect: typeof import('vue')['watchPostEffect'] + const watchSyncEffect: typeof import('vue')['watchSyncEffect'] +} +// for type re-export +declare global { + // @ts-ignore + export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue' + import('vue') +} diff --git a/frontend/components.d.ts b/frontend/components.d.ts new file mode 100644 index 0000000..cde4d8b --- /dev/null +++ b/frontend/components.d.ts @@ -0,0 +1,70 @@ +/* eslint-disable */ +/* prettier-ignore */ +// @ts-nocheck +// Generated by unplugin-vue-components +// Read more: https://github.com/vuejs/core/pull/3399 +export {} + +declare module 'vue' { + export interface GlobalComponents { + AgentManagement: typeof import('./src/components/AgentManagement.vue')['default'] + AgentWorkflow: typeof import('./src/components/AgentWorkflow.vue')['default'] + CreativeStudio: typeof import('./src/components/CreativeStudio.vue')['default'] + DepartmentManagement: typeof import('./src/components/system/DepartmentManagement.vue')['default'] + ElAlert: typeof import('element-plus/es')['ElAlert'] + ElAvatar: typeof import('element-plus/es')['ElAvatar'] + ElButton: typeof import('element-plus/es')['ElButton'] + ElButtonGroup: typeof import('element-plus/es')['ElButtonGroup'] + ElCard: typeof import('element-plus/es')['ElCard'] + ElCol: typeof import('element-plus/es')['ElCol'] + ElCollapse: typeof import('element-plus/es')['ElCollapse'] + ElCollapseItem: typeof import('element-plus/es')['ElCollapseItem'] + ElDialog: typeof import('element-plus/es')['ElDialog'] + ElDivider: typeof import('element-plus/es')['ElDivider'] + ElDrawer: typeof import('element-plus/es')['ElDrawer'] + ElDropdown: typeof import('element-plus/es')['ElDropdown'] + ElDropdownItem: typeof import('element-plus/es')['ElDropdownItem'] + ElDropdownMenu: typeof import('element-plus/es')['ElDropdownMenu'] + ElEmpty: typeof import('element-plus/es')['ElEmpty'] + ElForm: typeof import('element-plus/es')['ElForm'] + ElFormItem: typeof import('element-plus/es')['ElFormItem'] + ElIcon: typeof import('element-plus/es')['ElIcon'] + ElInput: typeof import('element-plus/es')['ElInput'] + ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] + ElLink: typeof import('element-plus/es')['ElLink'] + ElOption: typeof import('element-plus/es')['ElOption'] + ElPagination: typeof import('element-plus/es')['ElPagination'] + ElProgress: typeof import('element-plus/es')['ElProgress'] + ElRadioButton: typeof import('element-plus/es')['ElRadioButton'] + ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] + ElRow: typeof import('element-plus/es')['ElRow'] + ElSelect: typeof import('element-plus/es')['ElSelect'] + ElSkeleton: typeof import('element-plus/es')['ElSkeleton'] + ElSlider: typeof import('element-plus/es')['ElSlider'] + ElSwitch: typeof import('element-plus/es')['ElSwitch'] + ElTable: typeof import('element-plus/es')['ElTable'] + ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] + ElTabPane: typeof import('element-plus/es')['ElTabPane'] + ElTabs: typeof import('element-plus/es')['ElTabs'] + ElTag: typeof import('element-plus/es')['ElTag'] + ElTooltip: typeof import('element-plus/es')['ElTooltip'] + ElTreeSelect: typeof import('element-plus/es')['ElTreeSelect'] + ElUpload: typeof import('element-plus/es')['ElUpload'] + KnowledgeManagement: typeof import('./src/components/KnowledgeManagement.vue')['default'] + LLMConfigManagement: typeof import('./src/components/system/LLMConfigManagement.vue')['default'] + MainLayout: typeof import('./src/components/MainLayout.vue')['default'] + NodeParameterConfig: typeof import('./src/components/NodeParameterConfig.vue')['default'] + NotFound: typeof import('./src/components/NotFound.vue')['default'] + ParameterInputDialog: typeof import('./src/components/ParameterInputDialog.vue')['default'] + ProfileDialog: typeof import('./src/components/ProfileDialog.vue')['default'] + RoleManagement: typeof import('./src/components/system/RoleManagement.vue')['default'] + RouterLink: typeof import('vue-router')['RouterLink'] + RouterView: typeof import('vue-router')['RouterView'] + SmartQuery: typeof import('./src/components/SmartQuery.vue')['default'] + UserManagement: typeof import('./src/components/system/UserManagement.vue')['default'] + WorkflowEditor: typeof import('./src/components/WorkflowEditor.vue')['default'] + } + export interface ComponentCustomProperties { + vLoading: typeof import('element-plus/es')['ElLoadingDirective'] + } +} diff --git a/frontend/env.d.ts b/frontend/env.d.ts new file mode 100644 index 0000000..2ea5417 --- /dev/null +++ b/frontend/env.d.ts @@ -0,0 +1,14 @@ +/// + +interface ImportMetaEnv { + readonly VITE_API_BASE_URL: string + readonly VITE_APP_TITLE: string + readonly VITE_APP_VERSION: string + readonly VITE_ENABLE_MOCK: string + readonly VITE_UPLOAD_MAX_SIZE: string + readonly VITE_SUPPORTED_FILE_TYPES: string +} + +interface ImportMeta { + readonly env: ImportMetaEnv +} \ No newline at end of file diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..39158b8 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,107 @@ + + + + + + + + + + TH智能对话助手 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +

+ + +
+ + + + + + + + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..3ec45a4 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,4493 @@ +{ + "name": "chat-agent-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "chat-agent-frontend", + "version": "0.1.0", + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "axios": "^1.6.0", + "dayjs": "^1.11.10", + "element-plus": "^2.4.2", + "highlight.js": "^11.9.0", + "lodash-es": "^4.17.21", + "markdown-it": "^13.0.2", + "nprogress": "^0.2.0", + "pinia": "^2.1.7", + "vue": "^3.3.8", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.4.1", + "@vue/eslint-config-prettier": "^8.0.0", + "@vue/eslint-config-typescript": "^12.0.0", + "@vue/tsconfig": "^0.4.0", + "eslint": "^8.53.0", + "eslint-plugin-vue": "^9.18.1", + "prettier": "^3.0.3", + "sass": "^1.91.0", + "typescript": "~5.2.0", + "unplugin-auto-import": "^0.16.7", + "unplugin-vue-components": "^0.25.2", + "vite": "^4.5.0", + "vue-tsc": "^1.8.22" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + } + }, + "node_modules/@antfu/utils": { + "version": "0.7.10", + "resolved": "https://registry.npmmirror.com/@antfu/utils/-/utils-0.7.10.tgz", + "integrity": "sha512-+562v9k4aI80m1+VuMHehNJWLOFjBnXn3tdOitzD0il5b7smkSBal4+a3oKiQTbrwMmN/TBUMDvbdoWDehgOww==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmmirror.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.3", + "resolved": "https://registry.npmmirror.com/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "3.6.1", + "resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz", + "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@element-plus/icons-vue": { + "version": "2.3.2", + "resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.2.tgz", + "integrity": "sha512-OzIuTaIfC8QXEPmJvB4Y4kw34rSXdCJzxcD1kFStBvr8bK6X1zQAYDo0CNMjojnfTqRQCJ0I7prlErcoRiET2A==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.18.20.tgz", + "integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz", + "integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.18.20.tgz", + "integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz", + "integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz", + "integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz", + "integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz", + "integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz", + "integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz", + "integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz", + "integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz", + "integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz", + "integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz", + "integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz", + "integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz", + "integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz", + "integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz", + "integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz", + "integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz", + "integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz", + "integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz", + "integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", + "integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmmirror.com/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmmirror.com/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmmirror.com/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmmirror.com/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.7.3", + "resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.3.tgz", + "integrity": "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.7.4", + "resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.4.tgz", + "integrity": "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.7.3", + "@floating-ui/utils": "^0.2.10" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.10", + "resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.10.tgz", + "integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==", + "license": "MIT" + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmmirror.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher/-/watcher-2.5.1.tgz", + "integrity": "sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.1", + "@parcel/watcher-darwin-arm64": "2.5.1", + "@parcel/watcher-darwin-x64": "2.5.1", + "@parcel/watcher-freebsd-x64": "2.5.1", + "@parcel/watcher-linux-arm-glibc": "2.5.1", + "@parcel/watcher-linux-arm-musl": "2.5.1", + "@parcel/watcher-linux-arm64-glibc": "2.5.1", + "@parcel/watcher-linux-arm64-musl": "2.5.1", + "@parcel/watcher-linux-x64-glibc": "2.5.1", + "@parcel/watcher-linux-x64-musl": "2.5.1", + "@parcel/watcher-win32-arm64": "2.5.1", + "@parcel/watcher-win32-ia32": "2.5.1", + "@parcel/watcher-win32-x64": "2.5.1" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", + "integrity": "sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.1.tgz", + "integrity": "sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmmirror.com/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmmirror.com/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@popperjs/core": { + "name": "@sxzz/popperjs-es", + "version": "2.11.7", + "resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz", + "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.2.0", + "resolved": "https://registry.npmmirror.com/@rollup/pluginutils/-/pluginutils-5.2.0.tgz", + "integrity": "sha512-qWJ2ZTbmumwiLFomfzTyt5Kng4hwPi9rwCYN4SHb6eaRU1KNO4ccxINHr/VhH4GgPlt1XfSTLX2LBTme8ne4Zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmmirror.com/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/lodash": { + "version": "4.17.20", + "resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.20.tgz", + "integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==", + "license": "MIT" + }, + "node_modules/@types/lodash-es": { + "version": "4.17.12", + "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", + "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", + "license": "MIT", + "dependencies": { + "@types/lodash": "*" + } + }, + "node_modules/@types/semver": { + "version": "7.7.0", + "resolved": "https://registry.npmmirror.com/@types/semver/-/semver-7.7.0.tgz", + "integrity": "sha512-k107IF4+Xr7UHjwDc7Cfd6PRQfbdkiRabXGRjo07b4WyPahFBZCZ1sE+BNxYIJPPg73UkfOsVOLwqVc/6ETrIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/web-bluetooth": { + "version": "0.0.16", + "resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz", + "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==", + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@vitejs/plugin-vue": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-4.6.2.tgz", + "integrity": "sha512-kqf7SGFoG+80aZG6Pf+gsZIVvGSCKE98JbiWqcCV9cThtg91Jav0yvYFC9Zb+jKetNGF6ZKeoaxgZfND21fWKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.0.0 || ^5.0.0", + "vue": "^3.2.25" + } + }, + "node_modules/@volar/language-core": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@volar/language-core/-/language-core-1.11.1.tgz", + "integrity": "sha512-dOcNn3i9GgZAcJt43wuaEykSluAuOkQgzni1cuxLxTV0nJKanQztp7FxyswdRILaKH+P2XZMPRp2S4MV/pElCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/source-map": "1.11.1" + } + }, + "node_modules/@volar/source-map": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@volar/source-map/-/source-map-1.11.1.tgz", + "integrity": "sha512-hJnOnwZ4+WT5iupLRnuzbULZ42L7BWWPMmruzwtLhJfpDVoZLjNBxHDi2sY2bgZXCKlpU5XcsMFoYrsQmPhfZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "muggle-string": "^0.3.1" + } + }, + "node_modules/@volar/typescript": { + "version": "1.11.1", + "resolved": "https://registry.npmmirror.com/@volar/typescript/-/typescript-1.11.1.tgz", + "integrity": "sha512-iU+t2mas/4lYierSnoFOeRFQUhAEMgsFuQxoxvwn5EdQopw43j+J27a4lt9LMInx1gLJBC6qL14WYGlgymaSMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "1.11.1", + "path-browserify": "^1.0.1" + } + }, + "node_modules/@vue/compiler-core": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/@vue/compiler-core/-/compiler-core-3.5.20.tgz", + "integrity": "sha512-8TWXUyiqFd3GmP4JTX9hbiTFRwYHgVL/vr3cqhr4YQ258+9FADwvj7golk2sWNGHR67QgmCZ8gz80nQcMokhwg==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/shared": "3.5.20", + "entities": "^4.5.0", + "estree-walker": "^2.0.2", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-core/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/@vue/compiler-dom": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/@vue/compiler-dom/-/compiler-dom-3.5.20.tgz", + "integrity": "sha512-whB44M59XKjqUEYOMPYU0ijUV0G+4fdrHVKDe32abNdX/kJe1NUEMqsi4cwzXa9kyM9w5S8WqFsrfo1ogtBZGQ==", + "license": "MIT", + "dependencies": { + "@vue/compiler-core": "3.5.20", + "@vue/shared": "3.5.20" + } + }, + "node_modules/@vue/compiler-sfc": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.20.tgz", + "integrity": "sha512-SFcxapQc0/feWiSBfkGsa1v4DOrnMAQSYuvDMpEaxbpH5dKbnEM5KobSNSgU+1MbHCl+9ftm7oQWxvwDB6iBfw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@vue/compiler-core": "3.5.20", + "@vue/compiler-dom": "3.5.20", + "@vue/compiler-ssr": "3.5.20", + "@vue/shared": "3.5.20", + "estree-walker": "^2.0.2", + "magic-string": "^0.30.17", + "postcss": "^8.5.6", + "source-map-js": "^1.2.1" + } + }, + "node_modules/@vue/compiler-ssr": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/@vue/compiler-ssr/-/compiler-ssr-3.5.20.tgz", + "integrity": "sha512-RSl5XAMc5YFUXpDQi+UQDdVjH9FnEpLDHIALg5J0ITHxkEzJ8uQLlo7CIbjPYqmZtt6w0TsIPbo1izYXwDG7JA==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.20", + "@vue/shared": "3.5.20" + } + }, + "node_modules/@vue/devtools-api": { + "version": "6.6.4", + "resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz", + "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==", + "license": "MIT" + }, + "node_modules/@vue/eslint-config-prettier": { + "version": "8.0.0", + "resolved": "https://registry.npmmirror.com/@vue/eslint-config-prettier/-/eslint-config-prettier-8.0.0.tgz", + "integrity": "sha512-55dPqtC4PM/yBjhAr+yEw6+7KzzdkBuLmnhBrDfp4I48+wy+Giqqj9yUr5T2uD/BkBROjjmqnLZmXRdOx/VtQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-config-prettier": "^8.8.0", + "eslint-plugin-prettier": "^5.0.0" + }, + "peerDependencies": { + "eslint": ">= 8.0.0", + "prettier": ">= 3.0.0" + } + }, + "node_modules/@vue/eslint-config-typescript": { + "version": "12.0.0", + "resolved": "https://registry.npmmirror.com/@vue/eslint-config-typescript/-/eslint-config-typescript-12.0.0.tgz", + "integrity": "sha512-StxLFet2Qe97T8+7L8pGlhYBBr8Eg05LPuTDVopQV6il+SK6qqom59BA/rcFipUef2jD8P2X44Vd8tMFytfvlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "^6.7.0", + "@typescript-eslint/parser": "^6.7.0", + "vue-eslint-parser": "^9.3.1" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0", + "eslint-plugin-vue": "^9.0.0", + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/language-core": { + "version": "1.8.27", + "resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-1.8.27.tgz", + "integrity": "sha512-L8Kc27VdQserNaCUNiSFdDl9LWT24ly8Hpwf1ECy3aFb9m6bDhBGQYOujDm21N7EW3moKIOKEanQwe1q5BK+mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/language-core": "~1.11.1", + "@volar/source-map": "~1.11.1", + "@vue/compiler-dom": "^3.3.0", + "@vue/shared": "^3.3.0", + "computeds": "^0.0.1", + "minimatch": "^9.0.3", + "muggle-string": "^0.3.1", + "path-browserify": "^1.0.1", + "vue-template-compiler": "^2.7.14" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@vue/reactivity": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/@vue/reactivity/-/reactivity-3.5.20.tgz", + "integrity": "sha512-hS8l8x4cl1fmZpSQX/NXlqWKARqEsNmfkwOIYqtR2F616NGfsLUm0G6FQBK6uDKUCVyi1YOL8Xmt/RkZcd/jYQ==", + "license": "MIT", + "dependencies": { + "@vue/shared": "3.5.20" + } + }, + "node_modules/@vue/runtime-core": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/@vue/runtime-core/-/runtime-core-3.5.20.tgz", + "integrity": "sha512-vyQRiH5uSZlOa+4I/t4Qw/SsD/gbth0SW2J7oMeVlMFMAmsG1rwDD6ok0VMmjXY3eI0iHNSSOBilEDW98PLRKw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.20", + "@vue/shared": "3.5.20" + } + }, + "node_modules/@vue/runtime-dom": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/@vue/runtime-dom/-/runtime-dom-3.5.20.tgz", + "integrity": "sha512-KBHzPld/Djw3im0CQ7tGCpgRedryIn4CcAl047EhFTCCPT2xFf4e8j6WeKLgEEoqPSl9TYqShc3Q6tpWpz/Xgw==", + "license": "MIT", + "dependencies": { + "@vue/reactivity": "3.5.20", + "@vue/runtime-core": "3.5.20", + "@vue/shared": "3.5.20", + "csstype": "^3.1.3" + } + }, + "node_modules/@vue/server-renderer": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/@vue/server-renderer/-/server-renderer-3.5.20.tgz", + "integrity": "sha512-HthAS0lZJDH21HFJBVNTtx+ULcIbJQRpjSVomVjfyPkFSpCwvsPTA+jIzOaUm3Hrqx36ozBHePztQFg6pj5aKg==", + "license": "MIT", + "dependencies": { + "@vue/compiler-ssr": "3.5.20", + "@vue/shared": "3.5.20" + }, + "peerDependencies": { + "vue": "3.5.20" + } + }, + "node_modules/@vue/shared": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/@vue/shared/-/shared-3.5.20.tgz", + "integrity": "sha512-SoRGP596KU/ig6TfgkCMbXkr4YJ91n/QSdMuqeP5r3hVIYA3CPHUBCc7Skak0EAKV+5lL4KyIh61VA/pK1CIAA==", + "license": "MIT" + }, + "node_modules/@vue/tsconfig": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/@vue/tsconfig/-/tsconfig-0.4.0.tgz", + "integrity": "sha512-CPuIReonid9+zOG/CGTT05FXrPYATEqoDGNrEaqS4hwcw5BUNM2FguC0mOwJD4Jr16UpRVl9N0pY3P+srIbqmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@vueuse/core": { + "version": "9.13.0", + "resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz", + "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==", + "license": "MIT", + "dependencies": { + "@types/web-bluetooth": "^0.0.16", + "@vueuse/metadata": "9.13.0", + "@vueuse/shared": "9.13.0", + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/metadata": { + "version": "9.13.0", + "resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz", + "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/@vueuse/shared": { + "version": "9.13.0", + "resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz", + "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==", + "license": "MIT", + "dependencies": { + "vue-demi": "*" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmmirror.com/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async-validator": { + "version": "4.2.5", + "resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz", + "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==", + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmmirror.com/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/computeds": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/computeds/-/computeds-0.0.1.tgz", + "integrity": "sha512-7CEBgcMjVmitjYo5q8JTJVra6X5mQ20uTThdK+0kR7UEaDrAWEQcRiBtWJzga4eRpP6afNwwLsX2SET2JhVB1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "license": "MIT" + }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, + "node_modules/de-indent": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz", + "integrity": "sha512-e/1zu3xH5MQryN2zdVaF0OrdNLUbvWxzMbi+iNA6Bky7l1RoP8a2fIbRocyHclXt/arDrrR6lL3TqFD9pMQTsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/element-plus": { + "version": "2.11.1", + "resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.11.1.tgz", + "integrity": "sha512-weYFIniyNXTAe9vJZnmZpYzurh4TDbdKhBsJwhbzuo0SDZ8PLwHVll0qycJUxc6SLtH+7A9F7dvdDh5CnqeIVA==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^3.4.1", + "@element-plus/icons-vue": "^2.3.1", + "@floating-ui/dom": "^1.0.1", + "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7", + "@types/lodash": "^4.14.182", + "@types/lodash-es": "^4.17.6", + "@vueuse/core": "^9.1.0", + "async-validator": "^4.2.5", + "dayjs": "^1.11.13", + "escape-html": "^1.0.3", + "lodash": "^4.17.21", + "lodash-es": "^4.17.21", + "lodash-unified": "^1.0.2", + "memoize-one": "^6.0.0", + "normalize-wheel-es": "^1.2.0" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.18.20", + "resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.18.20.tgz", + "integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.18.20", + "@esbuild/android-arm64": "0.18.20", + "@esbuild/android-x64": "0.18.20", + "@esbuild/darwin-arm64": "0.18.20", + "@esbuild/darwin-x64": "0.18.20", + "@esbuild/freebsd-arm64": "0.18.20", + "@esbuild/freebsd-x64": "0.18.20", + "@esbuild/linux-arm": "0.18.20", + "@esbuild/linux-arm64": "0.18.20", + "@esbuild/linux-ia32": "0.18.20", + "@esbuild/linux-loong64": "0.18.20", + "@esbuild/linux-mips64el": "0.18.20", + "@esbuild/linux-ppc64": "0.18.20", + "@esbuild/linux-riscv64": "0.18.20", + "@esbuild/linux-s390x": "0.18.20", + "@esbuild/linux-x64": "0.18.20", + "@esbuild/netbsd-x64": "0.18.20", + "@esbuild/openbsd-x64": "0.18.20", + "@esbuild/sunos-x64": "0.18.20", + "@esbuild/win32-arm64": "0.18.20", + "@esbuild/win32-ia32": "0.18.20", + "@esbuild/win32-x64": "0.18.20" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmmirror.com/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.2", + "resolved": "https://registry.npmmirror.com/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz", + "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmmirror.com/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-vue": { + "version": "9.33.0", + "resolved": "https://registry.npmmirror.com/eslint-plugin-vue/-/eslint-plugin-vue-9.33.0.tgz", + "integrity": "sha512-174lJKuNsuDIlLpjeXc5E2Tss8P44uIimAfGD0b90k0NoirJqpG7stLuU9Vp/9ioTOrQdWVREc4mRd1BD+CvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "globals": "^13.24.0", + "natural-compare": "^1.4.0", + "nth-check": "^2.1.1", + "postcss-selector-parser": "^6.0.15", + "semver": "^7.6.3", + "vue-eslint-parser": "^9.4.3", + "xml-name-validator": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.2.0 || ^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmmirror.com/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmmirror.com/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmmirror.com/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmmirror.com/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exsolve": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.7.tgz", + "integrity": "sha512-VO5fQUzZtI6C+vx4w/4BWJpg3s/5l+6pRQEHzFRM8WFi4XffSP1Z+4qi7GbjWbvRQEbdIco5mIMq+zX4rPuLrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmmirror.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmmirror.com/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmmirror.com/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmmirror.com/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmmirror.com/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmmirror.com/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmmirror.com/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/highlight.js": { + "version": "11.11.1", + "resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz", + "integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmmirror.com/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/immutable": { + "version": "5.1.3", + "resolved": "https://registry.npmmirror.com/immutable/-/immutable-5.1.3.tgz", + "integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmmirror.com/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmmirror.com/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/linkify-it": { + "version": "4.0.1", + "resolved": "https://registry.npmmirror.com/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "license": "MIT", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/local-pkg": { + "version": "0.5.1", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.5.1.tgz", + "integrity": "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.3", + "pkg-types": "^1.2.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lodash-unified": { + "version": "1.0.3", + "resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz", + "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==", + "license": "MIT", + "peerDependencies": { + "@types/lodash-es": "*", + "lodash": "*", + "lodash-es": "*" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmmirror.com/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.18", + "resolved": "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.18.tgz", + "integrity": "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/markdown-it": { + "version": "13.0.2", + "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-13.0.2.tgz", + "integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==", + "license": "MIT" + }, + "node_modules/memoize-one": { + "version": "6.0.0", + "resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz", + "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmmirror.com/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmmirror.com/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mlly": { + "version": "1.8.0", + "resolved": "https://registry.npmmirror.com/mlly/-/mlly-1.8.0.tgz", + "integrity": "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.15.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.1" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/muggle-string": { + "version": "0.3.1", + "resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.3.1.tgz", + "integrity": "sha512-ckmWDJjphvd/FvZawgygcUeQCxzvohjFO5RxTjj4eq8kw359gFF3E1brjfI+viLMxss5JrHTDRHZvu2/tuy0Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmmirror.com/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-wheel-es": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz", + "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==", + "license": "BSD-3-Clause" + }, + "node_modules/nprogress": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/nprogress/-/nprogress-0.2.0.tgz", + "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", + "license": "MIT" + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmmirror.com/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-browserify": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz", + "integrity": "sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pinia": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/pinia/-/pinia-2.3.1.tgz", + "integrity": "sha512-khUlZSwt9xXCaTbbxFYBKDc/bWAGWJjOgvxETwkTN7KRm66EeT1ZdZj6i2ceh9sP2Pzqsbc704r2yngBrxBVug==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.3", + "vue-demi": "^0.14.10" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "typescript": ">=4.4.4", + "vue": "^2.7.0 || ^3.5.11" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "node_modules/postcss": { + "version": "8.5.6", + "resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", + "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmmirror.com/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quansync": { + "version": "0.2.11", + "resolved": "https://registry.npmmirror.com/quansync/-/quansync-0.2.11.tgz", + "integrity": "sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmmirror.com/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmmirror.com/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmmirror.com/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "3.29.5", + "resolved": "https://registry.npmmirror.com/rollup/-/rollup-3.29.5.tgz", + "integrity": "sha512-GVsDdsbJzzy4S/v3dqWPJ7EfvZJfCHiDqe80IyrF59LYuP+e6U1LJoUqeuqRbwAWoMNoXivMNeNAOf5E22VA1w==", + "dev": true, + "license": "MIT", + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmmirror.com/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/sass": { + "version": "1.91.0", + "resolved": "https://registry.npmmirror.com/sass/-/sass-1.91.0.tgz", + "integrity": "sha512-aFOZHGf+ur+bp1bCHZ+u8otKGh77ZtmFyXDo4tlYvT7PWql41Kwd8wdkPqhhT+h2879IVblcHFglIMofsFd1EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/scule": { + "version": "1.3.0", + "resolved": "https://registry.npmmirror.com/scule/-/scule-1.3.0.tgz", + "integrity": "sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmmirror.com/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmmirror.com/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmmirror.com/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "2.1.1", + "resolved": "https://registry.npmmirror.com/strip-literal/-/strip-literal-2.1.1.tgz", + "integrity": "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmmirror.com/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmmirror.com/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmmirror.com/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmmirror.com/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmmirror.com/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.2.2", + "resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.2.2.tgz", + "integrity": "sha512-mI4WrpHsbCIcwT9cF4FZvr80QUeKvsUsUvKDoR+X/7XHQH98xYD8YHZg7ANtz2GtZt/CBq2QJ0thkGJMHfqc1w==", + "devOptional": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "1.0.6", + "resolved": "https://registry.npmmirror.com/uc.micro/-/uc.micro-1.0.6.tgz", + "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", + "license": "MIT" + }, + "node_modules/ufo": { + "version": "1.6.1", + "resolved": "https://registry.npmmirror.com/ufo/-/ufo-1.6.1.tgz", + "integrity": "sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/unimport": { + "version": "3.14.6", + "resolved": "https://registry.npmmirror.com/unimport/-/unimport-3.14.6.tgz", + "integrity": "sha512-CYvbDaTT04Rh8bmD8jz3WPmHYZRG/NnvYVzwD6V1YAlvvKROlAeNDUBhkBGzNav2RKaeuXvlWYaa1V4Lfi/O0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.1.4", + "acorn": "^8.14.0", + "escape-string-regexp": "^5.0.0", + "estree-walker": "^3.0.3", + "fast-glob": "^3.3.3", + "local-pkg": "^1.0.0", + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "pathe": "^2.0.1", + "picomatch": "^4.0.2", + "pkg-types": "^1.3.0", + "scule": "^1.3.0", + "strip-literal": "^2.1.1", + "unplugin": "^1.16.1" + } + }, + "node_modules/unimport/node_modules/confbox": { + "version": "0.2.2", + "resolved": "https://registry.npmmirror.com/confbox/-/confbox-0.2.2.tgz", + "integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unimport/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unimport/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/unimport/node_modules/local-pkg": { + "version": "1.1.2", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-1.1.2.tgz", + "integrity": "sha512-arhlxbFRmoQHl33a0Zkle/YWlmNwoyt6QNZEIJcqNbdrsix5Lvc4HyyI3EnwxTYlZYc32EbYrQ8SzEZ7dqgg9A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mlly": "^1.7.4", + "pkg-types": "^2.3.0", + "quansync": "^0.2.11" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unimport/node_modules/local-pkg/node_modules/pkg-types": { + "version": "2.3.0", + "resolved": "https://registry.npmmirror.com/pkg-types/-/pkg-types-2.3.0.tgz", + "integrity": "sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.2.2", + "exsolve": "^1.0.7", + "pathe": "^2.0.3" + } + }, + "node_modules/unimport/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmmirror.com/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/unplugin": { + "version": "1.16.1", + "resolved": "https://registry.npmmirror.com/unplugin/-/unplugin-1.16.1.tgz", + "integrity": "sha512-4/u/j4FrCKdi17jaxuJA0jClGxB1AvU2hw/IuayPc4ay1XGaJs/rbb4v5WKwAjNifjmXK9PIFyuPiaK8azyR9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.14.0", + "webpack-virtual-modules": "^0.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/unplugin-auto-import": { + "version": "0.16.7", + "resolved": "https://registry.npmmirror.com/unplugin-auto-import/-/unplugin-auto-import-0.16.7.tgz", + "integrity": "sha512-w7XmnRlchq6YUFJVFGSvG1T/6j8GrdYN6Em9Wf0Ye+HXgD/22kont+WnuCAA0UaUoxtuvRR1u/mXKy63g/hfqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.6", + "@rollup/pluginutils": "^5.0.5", + "fast-glob": "^3.3.1", + "local-pkg": "^0.5.0", + "magic-string": "^0.30.5", + "minimatch": "^9.0.3", + "unimport": "^3.4.0", + "unplugin": "^1.5.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@nuxt/kit": "^3.2.2", + "@vueuse/core": "*" + }, + "peerDependenciesMeta": { + "@nuxt/kit": { + "optional": true + }, + "@vueuse/core": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components": { + "version": "0.25.2", + "resolved": "https://registry.npmmirror.com/unplugin-vue-components/-/unplugin-vue-components-0.25.2.tgz", + "integrity": "sha512-OVmLFqILH6w+eM8fyt/d/eoJT9A6WO51NZLf1vC5c1FZ4rmq2bbGxTy8WP2Jm7xwFdukaIdv819+UI7RClPyCA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@antfu/utils": "^0.7.5", + "@rollup/pluginutils": "^5.0.2", + "chokidar": "^3.5.3", + "debug": "^4.3.4", + "fast-glob": "^3.3.0", + "local-pkg": "^0.4.3", + "magic-string": "^0.30.1", + "minimatch": "^9.0.3", + "resolve": "^1.22.2", + "unplugin": "^1.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@babel/parser": "^7.15.8", + "@nuxt/kit": "^3.2.2", + "vue": "2 || 3" + }, + "peerDependenciesMeta": { + "@babel/parser": { + "optional": true + }, + "@nuxt/kit": { + "optional": true + } + } + }, + "node_modules/unplugin-vue-components/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/unplugin-vue-components/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmmirror.com/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/unplugin-vue-components/node_modules/local-pkg": { + "version": "0.4.3", + "resolved": "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.4.3.tgz", + "integrity": "sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/unplugin-vue-components/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmmirror.com/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "4.5.14", + "resolved": "https://registry.npmmirror.com/vite/-/vite-4.5.14.tgz", + "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.18.10", + "postcss": "^8.4.27", + "rollup": "^3.27.1" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vue": { + "version": "3.5.20", + "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.20.tgz", + "integrity": "sha512-2sBz0x/wis5TkF1XZ2vH25zWq3G1bFEPOfkBcx2ikowmphoQsPH6X0V3mmPCXA2K1N/XGTnifVyDQP4GfDDeQw==", + "license": "MIT", + "dependencies": { + "@vue/compiler-dom": "3.5.20", + "@vue/compiler-sfc": "3.5.20", + "@vue/runtime-dom": "3.5.20", + "@vue/server-renderer": "3.5.20", + "@vue/shared": "3.5.20" + }, + "peerDependencies": { + "typescript": "*" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/vue-demi": { + "version": "0.14.10", + "resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz", + "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "vue-demi-fix": "bin/vue-demi-fix.js", + "vue-demi-switch": "bin/vue-demi-switch.js" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + }, + "peerDependencies": { + "@vue/composition-api": "^1.0.0-rc.1", + "vue": "^3.0.0-0 || ^2.6.0" + }, + "peerDependenciesMeta": { + "@vue/composition-api": { + "optional": true + } + } + }, + "node_modules/vue-eslint-parser": { + "version": "9.4.3", + "resolved": "https://registry.npmmirror.com/vue-eslint-parser/-/vue-eslint-parser-9.4.3.tgz", + "integrity": "sha512-2rYRLWlIpaiN8xbPiDyXZXRgLGOtWxERV7ND5fFAv5qo1D2N9Fu9MNajBNc6o13lZ+24DAWCkQCvj4klgmcITg==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "eslint-scope": "^7.1.1", + "eslint-visitor-keys": "^3.3.0", + "espree": "^9.3.1", + "esquery": "^1.4.0", + "lodash": "^4.17.21", + "semver": "^7.3.6" + }, + "engines": { + "node": "^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/mysticatea" + }, + "peerDependencies": { + "eslint": ">=6.0.0" + } + }, + "node_modules/vue-router": { + "version": "4.5.1", + "resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz", + "integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==", + "license": "MIT", + "dependencies": { + "@vue/devtools-api": "^6.6.4" + }, + "funding": { + "url": "https://github.com/sponsors/posva" + }, + "peerDependencies": { + "vue": "^3.2.0" + } + }, + "node_modules/vue-template-compiler": { + "version": "2.7.16", + "resolved": "https://registry.npmmirror.com/vue-template-compiler/-/vue-template-compiler-2.7.16.tgz", + "integrity": "sha512-AYbUWAJHLGGQM7+cNTELw+KsOG9nl2CnSv467WobS5Cv9uk3wFcnr1Etsz2sEIHEZvw1U+o9mRlEO6QbZvUPGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "de-indent": "^1.0.2", + "he": "^1.2.0" + } + }, + "node_modules/vue-tsc": { + "version": "1.8.27", + "resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-1.8.27.tgz", + "integrity": "sha512-WesKCAZCRAbmmhuGl3+VrdWItEvfoFIPXOvUJkjULi+x+6G/Dy69yO3TBRJDr9eUlmsNAwVmxsNZxvHKzbkKdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@volar/typescript": "~1.11.1", + "@vue/language-core": "1.8.27", + "semver": "^7.5.4" + }, + "bin": { + "vue-tsc": "bin/vue-tsc.js" + }, + "peerDependencies": { + "typescript": "*" + } + }, + "node_modules/webpack-virtual-modules": { + "version": "0.6.2", + "resolved": "https://registry.npmmirror.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", + "integrity": "sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/xml-name-validator": { + "version": "4.0.0", + "resolved": "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz", + "integrity": "sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..be8b6db --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,46 @@ +{ + "name": "chat-agent-frontend", + "version": "0.1.0", + "description": "Vue.js frontend for TH-Agenter application", + "private": true, + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview", + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", + "format": "prettier --write src/", + "type-check": "vue-tsc --noEmit" + }, + "dependencies": { + "@element-plus/icons-vue": "^2.3.2", + "axios": "^1.6.0", + "dayjs": "^1.11.10", + "element-plus": "^2.4.2", + "highlight.js": "^11.9.0", + "lodash-es": "^4.17.21", + "markdown-it": "^13.0.2", + "nprogress": "^0.2.0", + "pinia": "^2.1.7", + "vue": "^3.3.8", + "vue-router": "^4.2.5" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^4.4.1", + "@vue/eslint-config-prettier": "^8.0.0", + "@vue/eslint-config-typescript": "^12.0.0", + "@vue/tsconfig": "^0.4.0", + "eslint": "^8.53.0", + "eslint-plugin-vue": "^9.18.1", + "prettier": "^3.0.3", + "sass": "^1.91.0", + "typescript": "~5.2.0", + "unplugin-auto-import": "^0.16.7", + "unplugin-vue-components": "^0.25.2", + "vite": "^4.5.0", + "vue-tsc": "^1.8.22" + }, + "engines": { + "node": ">=16.0.0", + "npm": ">=8.0.0" + } +} diff --git a/frontend/src/App.vue b/frontend/src/App.vue new file mode 100644 index 0000000..a498f4f --- /dev/null +++ b/frontend/src/App.vue @@ -0,0 +1,111 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/api/auth.ts b/frontend/src/api/auth.ts new file mode 100644 index 0000000..219f222 --- /dev/null +++ b/frontend/src/api/auth.ts @@ -0,0 +1,35 @@ +import { api } from './request' +import type { User, UserLogin, UserCreate, AuthTokens, ApiResponse, LoginRequest } from '@/types' + +// Auth API +export const authApi = { + // User login + login(data: LoginRequest) { + return api.post('/auth/login', data) + }, + + // User register + register(data: UserCreate) { + return api.post('/auth/register', data) + }, + + // Refresh token + refreshToken(refreshToken: string) { + return api.post('/auth/refresh', { + refresh_token: refreshToken + }) + }, + + // Get current user info + getCurrentUser() { + return api.get('/auth/me') + }, + + // Logout (optional - mainly client-side) + logout() { + // Clear tokens from localStorage + localStorage.removeItem('access_token') + localStorage.removeItem('refresh_token') + return Promise.resolve() + } +} \ No newline at end of file diff --git a/frontend/src/api/chat.ts b/frontend/src/api/chat.ts new file mode 100644 index 0000000..789eb21 --- /dev/null +++ b/frontend/src/api/chat.ts @@ -0,0 +1,109 @@ +import { api } from './request' +import type { + Conversation, + ConversationCreate, + ConversationUpdate, + Message, + ChatRequest, + ChatResponse, + PaginationParams +} from '@/types' + +// Chat API +export const chatApi = { + // Conversations + getConversations(params?: PaginationParams & { + search?: string + include_archived?: boolean + order_by?: string + order_desc?: boolean + }) { + return api.get('/chat/conversations', { params }) + }, + + getConversationsCount(params?: { + search?: string + include_archived?: boolean + }) { + return api.get<{ count: number }>('/chat/conversations/count', { params }) + }, + + createConversation(data: ConversationCreate) { + return api.post('/chat/conversations', data) + }, + + getConversation(conversationId: string) { + return api.get(`/chat/conversations/${conversationId}`) + }, + + updateConversation(conversationId: string, data: ConversationUpdate) { + return api.put(`/chat/conversations/${conversationId}`, data) + }, + + deleteConversation(conversationId: string) { + return api.delete(`/chat/conversations/${conversationId}`) + }, + + deleteAllConversations() { + return api.delete('/chat/conversations') + }, + + archiveConversation(conversationId: string) { + return api.put(`/chat/conversations/${conversationId}/archive`) + }, + + unarchiveConversation(conversationId: string) { + return api.put(`/chat/conversations/${conversationId}/unarchive`) + }, + + // Messages + getMessages(conversationId: string, params?: PaginationParams) { + return api.get(`/chat/conversations/${conversationId}/messages`, { params }) + }, + + // Chat + sendMessage(conversationId: string, data: ChatRequest) { + return api.post(`/chat/conversations/${conversationId}/chat`, data) + }, + + // Stream chat (for Server-Sent Events) + sendMessageStream(data: ChatRequest) { + const baseURL = import.meta.env.VITE_API_BASE_URL || '/api' + const token = localStorage.getItem('access_token') + + return new EventSource(`${baseURL}/chat/stream`, { + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + // Note: EventSource doesn't support POST with body directly + // This would need to be implemented differently, possibly using fetch with ReadableStream + }) + }, + + // Alternative stream implementation using fetch + async sendMessageStreamFetch(conversationId: string, data: ChatRequest): Promise | null> { + const baseURL = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api' + const token = localStorage.getItem('access_token') + + try { + const response = await fetch(`${baseURL}/chat/conversations/${conversationId}/chat/stream`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${token}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify(data) + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + return response.body + } catch (error) { + console.error('Stream request failed:', error) + throw error + } + } +} \ No newline at end of file diff --git a/frontend/src/api/departments.ts b/frontend/src/api/departments.ts new file mode 100644 index 0000000..fbbbcec --- /dev/null +++ b/frontend/src/api/departments.ts @@ -0,0 +1,97 @@ +import { api } from './request' +import type { PaginationParams } from '@/types' + +// Department types +export interface Department { + id: number + name: string + code: string + description?: string + parent_id?: number + manager_id?: number + is_active: boolean + created_at: string + updated_at: string + manager?: { + id: number + username: string + full_name?: string + } + children?: Department[] + user_count?: number +} + +export interface DepartmentCreate { + name: string + code: string + description?: string + parent_id?: number + manager_id?: number + is_active?: boolean + sort_order?: number +} + +export interface DepartmentUpdate { + name?: string + code?: string + description?: string + parent_id?: number + manager_id?: number + is_active?: boolean + sort_order?: number +} + +// Departments API +export const departmentsApi = { + // Get all departments + getDepartments(params?: { + skip?: number + limit?: number + search?: string + parent_id?: number + is_active?: boolean + include_children?: boolean + }) { + return api.get<{ + departments: Department[] + total: number + page: number + page_size: number + }>('/admin/departments/', { params }) + }, + + // Get department tree + getDepartmentTree() { + return api.get('/admin/departments/tree') + }, + + // Create new department + createDepartment(data: DepartmentCreate) { + return api.post('/admin/departments/', data) + }, + + // Get department by ID + getDepartmentById(departmentId: number) { + return api.get(`/admin/departments/${departmentId}`) + }, + + // Update department by ID + updateDepartmentById(departmentId: number, data: DepartmentUpdate) { + return api.put(`/admin/departments/${departmentId}`, data) + }, + + // Delete department by ID + deleteDepartmentById(departmentId: number) { + return api.delete(`/admin/departments/${departmentId}`) + }, + + // Get department users + getDepartmentUsers(departmentId: number, params?: PaginationParams) { + return api.get<{ + users: any[] + total: number + page: number + page_size: number + }>(`/admin/departments/${departmentId}/users`, { params }) + } +} \ No newline at end of file diff --git a/frontend/src/api/index.ts b/frontend/src/api/index.ts new file mode 100644 index 0000000..ee99084 --- /dev/null +++ b/frontend/src/api/index.ts @@ -0,0 +1,14 @@ +// Export all API modules +export { authApi } from './auth' +export { usersApi } from './users' +export { chatApi } from './chat' +export { knowledgeApi } from './knowledge' +export { rolesApi } from './roles' +export { departmentsApi } from './departments' +export { userDepartmentsApi } from './userDepartments' +export { llmConfigApi } from './llmConfig' +export { workflowApi } from './workflow' +export { api } from './request' + +// Re-export request instance as default +export { default } from './request' \ No newline at end of file diff --git a/frontend/src/api/knowledge.ts b/frontend/src/api/knowledge.ts new file mode 100644 index 0000000..68ba993 --- /dev/null +++ b/frontend/src/api/knowledge.ts @@ -0,0 +1,71 @@ +import { api } from './request' +import type { + KnowledgeBase, + KnowledgeBaseCreate, + KnowledgeBaseUpdate, + Document, + DocumentUpload, + DocumentListResponse, + DocumentChunksResponse, + SearchRequest, + SearchResult, + PaginationParams +} from '@/types' + +// Knowledge Base API +export const knowledgeApi = { + // Knowledge Bases + getKnowledgeBases(params?: PaginationParams) { + return api.get('/knowledge-bases/', { params }) + }, + + createKnowledgeBase(data: KnowledgeBaseCreate) { + return api.post('/knowledge-bases/', data) + }, + + getKnowledgeBase(knowledgeBaseId: string) { + return api.get(`/knowledge-bases/${knowledgeBaseId}`) + }, + + updateKnowledgeBase(knowledgeBaseId: string, data: KnowledgeBaseUpdate) { + return api.put(`/knowledge-bases/${knowledgeBaseId}`, data) + }, + + deleteKnowledgeBase(knowledgeBaseId: string) { + return api.delete(`/knowledge-bases/${knowledgeBaseId}`) + }, + + // Documents + getDocuments(knowledgeBaseId: string, params?: PaginationParams) { + return api.get(`/knowledge-bases/${knowledgeBaseId}/documents`, { params }) + }, + + uploadDocument(knowledgeBaseId: string, file: File) { + const formData = new FormData() + formData.append('file', file) + formData.append('knowledge_base_id', knowledgeBaseId) + + return api.upload(`/knowledge-bases/${knowledgeBaseId}/documents`, formData) + }, + + deleteDocument(knowledgeBaseId: string, documentId: string) { + return api.delete(`/knowledge-bases/${knowledgeBaseId}/documents/${documentId}`) + }, + + processDocument(knowledgeBaseId: string, documentId: string) { + return api.post(`/knowledge-bases/${knowledgeBaseId}/documents/${documentId}/process`) + }, + + getDocumentChunks(knowledgeBaseId: string, documentId: string) { + return api.get(`/knowledge-bases/${knowledgeBaseId}/documents/${documentId}/chunks`) + }, + + // Search + searchKnowledgeBase(data: SearchRequest) { + return api.post(`/knowledge-bases/${data.knowledge_base_id}/search`, { + query: data.query, + top_k: data.top_k, + score_threshold: data.score_threshold + }) + } +} \ No newline at end of file diff --git a/frontend/src/api/llmConfig.ts b/frontend/src/api/llmConfig.ts new file mode 100644 index 0000000..4b483f6 --- /dev/null +++ b/frontend/src/api/llmConfig.ts @@ -0,0 +1,156 @@ +import { api } from './request' +import type { PaginationParams } from '@/types' + +// LLM Configuration types +export interface LLMConfig { + id: number + name: string + provider: string + model_name: string + api_key: string + base_url?: string + temperature: number + max_tokens: number + top_p: number + description?: string + is_active: boolean + is_default: boolean + is_embedding: boolean + extra_config?: Record + created_at: string + updated_at: string +} + +export interface LLMConfigCreate { + name: string + provider: string + model_name: string + api_key: string + base_url?: string + temperature?: number + max_tokens?: number + top_p?: number + description?: string + is_active?: boolean + is_default?: boolean + is_embedding?: boolean + extra_config?: Record +} + +export interface LLMConfigUpdate { + name?: string + provider?: string + model_name?: string + api_key?: string + base_url?: string + temperature?: number + max_tokens?: number + top_p?: number + description?: string + is_active?: boolean + is_default?: boolean + is_embedding?: boolean + extra_config?: Record +} + +export interface LLMConfigTestRequest { + message?: string +} + +export interface LLMConfigTestResponse { + success: boolean + response_time: number + model_info?: string + error?: string + test_message?: any +} + +// LLM Configuration API +export const llmConfigApi = { + // Get all LLM configurations + getLLMConfigs(params?: { + skip?: number + limit?: number + search?: string + provider?: string + is_active?: boolean + is_embedding?: boolean + }) { + return api.get('/admin/llm-configs/', { params }) + }, + + // Get LLM configuration by ID + getLLMConfig(configId: number) { + return api.get(`/admin/llm-configs/${configId}`) + }, + + // Create new LLM configuration + createLLMConfig(data: LLMConfigCreate) { + return api.post('/admin/llm-configs/', data) + }, + + // Update LLM configuration + updateLLMConfig(configId: number, data: LLMConfigUpdate) { + return api.put(`/admin/llm-configs/${configId}`, data) + }, + + // Delete LLM configuration + deleteLLMConfig(configId: number) { + return api.delete(`/admin/llm-configs/${configId}`) + }, + + // Test LLM configuration + testLLMConfig(configId: number, data?: LLMConfigTestRequest) { + return api.post(`/admin/llm-configs/${configId}/test`, data || {}) + }, + + // Get embedding type configurations + getEmbeddingConfigs(params?: { + skip?: number + limit?: number + is_active?: boolean + }) { + return api.get('/admin/llm-configs/', { + params: { ...params, is_embedding: true } + }) + }, + + // Get chat model configurations + getChatConfigs(params?: { + skip?: number + limit?: number + is_active?: boolean + }) { + return api.get('/admin/llm-configs/', { + params: { ...params, is_embedding: false } + }) + }, + + // Get default configuration + getDefaultConfig(isEmbedding: boolean = false) { + return api.get('/admin/llm-configs/default', { + params: { is_embedding: isEmbedding } + }) + }, + + // Get active configurations + getActiveConfigs(params?: { + skip?: number + limit?: number + is_embedding?: boolean + }) { + return api.get('/admin/llm-configs/', { + params: { ...params, is_active: true } + }) + }, + + // Set as default configuration + setAsDefault(configId: number) { + return api.post(`/admin/llm-configs/${configId}/set-default`) + }, + + // Toggle configuration status + toggleStatus(configId: number) { + return api.post(`/admin/llm-configs/${configId}/toggle-status`) + } +} \ No newline at end of file diff --git a/frontend/src/api/request.ts b/frontend/src/api/request.ts new file mode 100644 index 0000000..2f6e2d6 --- /dev/null +++ b/frontend/src/api/request.ts @@ -0,0 +1,146 @@ +import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios' +import { ElMessage } from 'element-plus' +import router from '@/router' +import type { ApiResponse } from '@/types' + +// Create axios instance +const request = axios.create({ + baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api', + timeout: 30000, + headers: { + 'Content-Type': 'application/json' + } +}) + +// Request interceptor +request.interceptors.request.use( + (config: AxiosRequestConfig) => { + // Add auth token + const token = localStorage.getItem('access_token') + if (token && config.headers) { + config.headers.Authorization = `Bearer ${token}` + } + return config + }, + (error) => { + console.error('Request error:', error) + return Promise.reject(error) + } +) + +// 防止重复处理401错误的标志 +let isHandling401 = false + +// Response interceptor +request.interceptors.response.use( + (response: AxiosResponse) => { + const { data } = response + + // Handle successful response + if (data.success !== false) { + return response + } + + // Handle business logic errors + const message = data.message || data.error || 'Request failed' + ElMessage.error(message) + return Promise.reject(new Error(message)) + }, + async (error) => { + const { response, message } = error + + if (response) { + const { status, data } = response + + switch (status) { + case 401: + // 防止重复处理401错误 + if (!isHandling401) { + isHandling401 = true + + // 延迟处理,避免并发请求重复跳转 + setTimeout(() => { + console.log('认证失败,跳转到登录页面') + ElMessage.error('登录已过期,请重新登录') + + // 强制跳转到登录页面,添加expired参数标识token过期 + router.replace('/login?expired=true') + isHandling401 = false + }, 100) + } + break + + case 403: + ElMessage.error('没有权限访问该资源') + break + + case 404: + ElMessage.error('请求的资源不存在') + break + + case 400: + // Bad request errors - 支持detail字段 + const badRequestMsg = data?.detail || data?.message || data?.error || '请求参数错误' + ElMessage.error(badRequestMsg) + break + + case 422: + // Validation errors + const errorMessage = data?.detail?.[0]?.msg || data?.message || '请求参数错误' + ElMessage.error(errorMessage) + break + + case 500: + ElMessage.error('服务器内部错误') + break + + default: + const msg = data?.message || data?.error || `请求失败 (${status})` + ElMessage.error(msg) + } + } else if (message.includes('timeout')) { + ElMessage.error('请求超时,请稍后重试') + } else if (message.includes('Network Error')) { + ElMessage.error('网络连接失败,请检查网络') + } else { + ElMessage.error('请求失败,请稍后重试') + } + + return Promise.reject(error) + } +) + +// Request methods +export const api = { + get(url: string, config?: AxiosRequestConfig): Promise>> { + return request.get(url, config) + }, + + post(url: string, data?: any, config?: AxiosRequestConfig): Promise>> { + return request.post(url, data, config) + }, + + put(url: string, data?: any, config?: AxiosRequestConfig): Promise>> { + return request.put(url, data, config) + }, + + patch(url: string, data?: any, config?: AxiosRequestConfig): Promise>> { + return request.patch(url, data, config) + }, + + delete(url: string, config?: AxiosRequestConfig): Promise>> { + return request.delete(url, config) + }, + + upload(url: string, formData: FormData, config?: AxiosRequestConfig): Promise>> { + return request.post(url, formData, { + ...config, + headers: { + 'Content-Type': 'multipart/form-data', + ...config?.headers + } + }) + } +} + +export default request \ No newline at end of file diff --git a/frontend/src/api/roles.ts b/frontend/src/api/roles.ts new file mode 100644 index 0000000..5a1f024 --- /dev/null +++ b/frontend/src/api/roles.ts @@ -0,0 +1,80 @@ +import { api } from './request' +import type { Role, ApiResponse } from '@/types' + +// Role interfaces +export interface RoleCreate { + name: string + code: string + description?: string + sort_order?: number + is_active?: boolean +} + +export interface RoleUpdate { + name?: string + code?: string + description?: string + sort_order?: number + is_active?: boolean +} + +export interface UserRoleAssign { + user_id: number + role_ids: number[] +} + + + +// Roles API +export const rolesApi = { + // Get user roles by user ID + getUserRoles(userId: number) { + return api.get(`/admin/roles/user-roles/user/${userId}`) + }, + + // Get all roles + getRoles(params?: { + skip?: number + limit?: number + search?: string + is_active?: boolean + }) { + return api.get('/admin/roles/', { params }) + }, + + // Get role by ID + getRole(roleId: number) { + return api.get(`/admin/roles/${roleId}`) + }, + + // Create new role + createRole(data: RoleCreate) { + return api.post('/admin/roles/', data) + }, + + // Update role + updateRole(roleId: number, data: RoleUpdate) { + return api.put(`/admin/roles/${roleId}`, data) + }, + + // Delete role + deleteRole(roleId: number) { + return api.delete(`/admin/roles/${roleId}`) + }, + + // Assign permissions to role + assignRolePermissions(roleId: number, data: RolePermissionAssign) { + return api.post(`/admin/roles/${roleId}/permissions`, data) + }, + + // Get role permissions + getRolePermissions(roleId: number) { + return api.get(`/admin/roles/${roleId}/permissions`) + }, + + // Assign roles to user + assignUserRoles(data: UserRoleAssign) { + return api.post('/admin/roles/user-roles/assign', data) + }, + +} \ No newline at end of file diff --git a/frontend/src/api/userDepartments.ts b/frontend/src/api/userDepartments.ts new file mode 100644 index 0000000..9eeca47 --- /dev/null +++ b/frontend/src/api/userDepartments.ts @@ -0,0 +1,114 @@ +import { api } from './request' +import type { PaginationParams } from '@/types' + +// User Department types +export interface UserDepartment { + id: number + user_id: number + department_id: number + is_primary: boolean + is_active: boolean + created_at: string + updated_at: string +} + +export interface UserDepartmentWithDetails extends UserDepartment { + user_name?: string + user_email?: string + department_name?: string + department_code?: string +} + +export interface DepartmentUserList { + department_id: number + department_name: string + department_code: string + users: UserDepartmentWithDetails[] + total_users: number + active_users: number +} + +export interface UserDepartmentCreate { + user_id: number + department_id: number + is_primary?: boolean + is_active?: boolean +} + +export interface UserDepartmentUpdate { + is_primary?: boolean + is_active?: boolean +} + +// User Departments API +export const userDepartmentsApi = { + // Create user-department association + createUserDepartment(data: UserDepartmentCreate) { + return api.post('/admin/user-departments/', data) + }, + + // Get all departments for a user + getUserDepartments(userId: number, activeOnly: boolean = true) { + return api.get<{ + user_id: number + user_name: string + user_email: string + departments: UserDepartmentWithDetails[] + total_departments: number + active_departments: number + }>(`/admin/user-departments/user/${userId}`, { + params: { active_only: activeOnly } + }) + }, + + // Get all users in a department + getDepartmentUsers(departmentId: number, activeOnly: boolean = true) { + return api.get(`/admin/user-departments/department/${departmentId}`, { + params: { active_only: activeOnly } + }) + }, + + // Update user-department association + updateUserDepartment(userId: number, departmentId: number, data: UserDepartmentUpdate) { + return api.put(`/admin/user-departments/user/${userId}/department/${departmentId}`, data) + }, + + // Remove user from department + removeUserFromDepartment(userId: number, departmentId: number) { + return api.delete(`/admin/user-departments/user/${userId}/department/${departmentId}`) + }, + + // Set user's primary department + setUserPrimaryDepartment(userId: number, departmentId: number) { + return api.put(`/admin/user-departments/user/${userId}/primary-department`, { + department_id: departmentId + }) + }, + + // Bulk create user-department associations + bulkCreateUserDepartments(data: { + user_ids: number[] + department_id: number + is_primary?: boolean + is_active?: boolean + }) { + return api.post('/admin/user-departments/bulk', data) + }, + + // Get user's department tree + getUserDepartmentTree(userId: number) { + return api.get<{ + user_id: number + department_tree: any[] + }>(`/admin/user-departments/user/${userId}/tree`) + }, + + // Get all user IDs that have department associations + getUsersWithDepartments(activeOnly: boolean = true) { + return api.get<{ + user_ids: number[] + }>('/admin/user-departments/users-with-departments', { + params: { active_only: activeOnly } + }) + } +} \ No newline at end of file diff --git a/frontend/src/api/users.ts b/frontend/src/api/users.ts new file mode 100644 index 0000000..6b3a3ce --- /dev/null +++ b/frontend/src/api/users.ts @@ -0,0 +1,81 @@ +import { api } from './request' +import type { User, UserUpdate, UserCreate, PaginationParams } from '@/types' + +// Users API +export const usersApi = { + // Get current user profile + getProfile() { + return api.get('/users/profile') + }, + + // Update current user profile + updateProfile(data: UserUpdate) { + return api.put('/users/profile', data) + }, + + // Delete current user account + deleteAccount() { + return api.delete('/users/profile') + }, + + // Change password + changePassword(data: { current_password: string; new_password: string }) { + return api.put('/users/change-password', data) + }, + + // Admin: Get all users with pagination and filters + getUsers(params?: { + skip?: number + limit?: number + search?: string + department_id?: number + role_id?: number + is_active?: boolean + }) { + return api.get<{ + users: User[] + total: number + page: number + page_size: number + }>('/users/', { params }) + }, + + // Admin: Create new user + createUser(data: UserCreate & { + department_id?: number + is_admin?: boolean + is_active?: boolean + }) { + return api.post('/users/', data) + }, + + // Admin: Get user by ID + getUserById(userId: number) { + return api.get(`/users/${userId}`) + }, + + // Admin: Update user by ID + updateUserById(userId: number, data: UserUpdate & { + password?: string + department_id?: number + is_admin?: boolean + is_active?: boolean + }) { + return api.put(`/users/${userId}`, data) + }, + + // Admin: Delete user by ID + deleteUserById(userId: number) { + return api.delete(`/users/${userId}`) + }, + + // Admin: Update user status + updateUserStatus(userId: number, is_active: boolean) { + return api.put(`/users/${userId}`, { is_active }) + }, + + // Admin: Reset user password + resetUserPassword(userId: number, newPassword: string) { + return api.put(`/users/${userId}/reset-password`, { new_password: newPassword }) + } +} \ No newline at end of file diff --git a/frontend/src/api/workflow.ts b/frontend/src/api/workflow.ts new file mode 100644 index 0000000..ec665c7 --- /dev/null +++ b/frontend/src/api/workflow.ts @@ -0,0 +1,166 @@ +import { api } from './request' +import type { PaginationParams, NodeInputOutput } from '@/types' + +// 工作流类型定义 +export interface WorkflowNode { + id: string + type: 'start' | 'end' | 'llm' | 'condition' | 'code' | 'http' + name: string + description?: string + position: { + x: number + y: number + } + config: Record + parameters?: NodeInputOutput +} + +export interface WorkflowConnection { + id: string + from: string + to: string +} + +export interface WorkflowDefinition { + nodes: WorkflowNode[] + connections: WorkflowConnection[] +} + +export interface Workflow { + id: number + name: string + description?: string + version: string + status: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED' + definition: WorkflowDefinition + created_at: string + updated_at: string + created_by: number + updated_by: number +} + +export interface WorkflowCreate { + name: string + description?: string + version?: string + definition: WorkflowDefinition +} + +export interface WorkflowUpdate { + name?: string + description?: string + version?: string + definition?: WorkflowDefinition +} + +export interface WorkflowExecution { + id: number + workflow_id: number + status: 'pending' | 'running' | 'completed' | 'failed' + input_data?: Record + output_data?: Record + error_message?: string + started_at: string + completed_at?: string + executor_id: number + node_executions?: NodeExecution[] + created_at: string + updated_at: string +} + +export interface WorkflowExecuteRequest { + input_data?: Record +} + +export interface NodeExecution { + id: number + node_id: string + node_type: string + node_name: string + status: 'pending' | 'running' | 'completed' | 'failed' + input_data?: Record + output_data?: Record + error_message?: string + started_at?: string + completed_at?: string + duration_ms?: number +} + +// 工作流API +export const workflowApi = { + // 获取工作流列表 + getWorkflows(params?: PaginationParams & { + search?: string + status?: 'DRAFT' | 'PUBLISHED' | 'ARCHIVED' + created_by?: number + }) { + return api.get<{ + workflows: Workflow[] + total: number + page: number + size: number + }>('/workflows/', { params }) + }, + + // 获取工作流详情 + getWorkflow(workflowId: number) { + return api.get(`/workflows/${workflowId}`) + }, + + // 创建工作流 + createWorkflow(data: WorkflowCreate) { + return api.post('/workflows/', data) + }, + + // 更新工作流 + updateWorkflow(workflowId: number, data: WorkflowUpdate) { + return api.put(`/workflows/${workflowId}`, data) + }, + + // 删除工作流 + deleteWorkflow(workflowId: number) { + return api.delete(`/workflows/${workflowId}`) + }, + + // 激活工作流 + activateWorkflow(workflowId: number) { + return api.post(`/workflows/${workflowId}/activate`) + }, + + // 停用工作流 + deactivateWorkflow(workflowId: number) { + return api.post(`/workflows/${workflowId}/deactivate`) + }, + + // 执行工作流 + executeWorkflow(workflowId: number, data: WorkflowExecuteRequest) { + return api.post(`/workflows/${workflowId}/execute`, data) + }, + + // 获取工作流执行历史 + getWorkflowExecutions(workflowId: number, params?: PaginationParams) { + return api.get<{ + executions: WorkflowExecution[] + total: number + page: number + size: number + }>(`/workflows/${workflowId}/executions`, { params }) + }, + + // 获取执行详情 + getExecution(executionId: number) { + return api.get(`/workflows/executions/${executionId}`) + }, + + // 获取节点执行详情 + getNodeExecutions(executionId: number) { + return api.get<{ + node_executions: NodeExecution[] + }>(`/workflows/executions/${executionId}/nodes`) + }, + + // 停止执行 + stopExecution(executionId: number) { + return api.post(`/workflows/executions/${executionId}/stop`) + } +} \ No newline at end of file diff --git a/frontend/src/assets/logo.png b/frontend/src/assets/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d615841b2a7915202393e177f1d80f07d829becd GIT binary patch literal 8952 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 +
+
+

智能体管理

+
+ + + 导入智能体 + + + + 创建智能体 + +
+
+
+
+ + + + + + + + + + + + + + + + + + +
+ +
+ + 卡片 + 列表 + +
+
+
+ +
+ + + +
+
+
+
+ + +
+
+

{{ agent.name }}

+ {{ getCategoryText(agent.category) }} +
+
+ + {{ getStatusText(agent.status) }} + +
+
+ +
+

{{ agent.description }}

+
+
+ 对话数 + {{ agent.conversation_count }} +
+
+ 满意度 + {{ agent.satisfaction_rate }}% +
+
+
+ +
+ + + 测试 + + + + 编辑 + + + + + + + +
+
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + +
+ + + + + +
+
+ 头像 + +
+ + 上传头像 + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + + +
+ + + + + + + + + + + + + + + + + +
+
+ + + +
+
+

关联知识库

+ + + 添加知识库 + +
+ +
+
+
+ {{ kb.name }} + {{ kb.description }} +
+
+ + + +
+
+
+
+
+ + + +
+
+
+
+ +
+
+
{{ selectedAgent.conversation_count }}
+
总对话数
+
+
+ +
+
+ +
+
+
{{ selectedAgent.satisfaction_rate }}%
+
满意度
+
+
+ +
+
+ +
+
+
{{ selectedAgent.avg_response_time }}s
+
平均响应时间
+
+
+ +
+
+ +
+
+
{{ selectedAgent.active_users }}
+
活跃用户
+
+
+
+
+
+
+
+
+
+
+ + + + + \ No newline at end of file diff --git a/frontend/src/components/AgentWorkflow.vue b/frontend/src/components/AgentWorkflow.vue new file mode 100644 index 0000000..e7d5b0d --- /dev/null +++ b/frontend/src/components/AgentWorkflow.vue @@ -0,0 +1,845 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/CreativeStudio.vue b/frontend/src/components/CreativeStudio.vue new file mode 100644 index 0000000..c747f20 --- /dev/null +++ b/frontend/src/components/CreativeStudio.vue @@ -0,0 +1,1151 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/KnowledgeManagement.vue b/frontend/src/components/KnowledgeManagement.vue new file mode 100644 index 0000000..2a17755 --- /dev/null +++ b/frontend/src/components/KnowledgeManagement.vue @@ -0,0 +1,1726 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/MainLayout.vue b/frontend/src/components/MainLayout.vue new file mode 100644 index 0000000..dc6be41 --- /dev/null +++ b/frontend/src/components/MainLayout.vue @@ -0,0 +1,1019 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/NodeParameterConfig.vue b/frontend/src/components/NodeParameterConfig.vue new file mode 100644 index 0000000..0422e31 --- /dev/null +++ b/frontend/src/components/NodeParameterConfig.vue @@ -0,0 +1,894 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/NotFound.vue b/frontend/src/components/NotFound.vue new file mode 100644 index 0000000..bf70a7c --- /dev/null +++ b/frontend/src/components/NotFound.vue @@ -0,0 +1,176 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/ParameterInputDialog.vue b/frontend/src/components/ParameterInputDialog.vue new file mode 100644 index 0000000..55e437e --- /dev/null +++ b/frontend/src/components/ParameterInputDialog.vue @@ -0,0 +1,427 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/ProfileDialog.vue b/frontend/src/components/ProfileDialog.vue new file mode 100644 index 0000000..6ff41c4 --- /dev/null +++ b/frontend/src/components/ProfileDialog.vue @@ -0,0 +1,315 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/SmartQuery.vue b/frontend/src/components/SmartQuery.vue new file mode 100644 index 0000000..ec9484f --- /dev/null +++ b/frontend/src/components/SmartQuery.vue @@ -0,0 +1,4062 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/WorkflowEditor.vue b/frontend/src/components/WorkflowEditor.vue new file mode 100644 index 0000000..e8bd339 --- /dev/null +++ b/frontend/src/components/WorkflowEditor.vue @@ -0,0 +1,3967 @@ + + + + + + + \ No newline at end of file diff --git a/frontend/src/components/system/DepartmentManagement.vue b/frontend/src/components/system/DepartmentManagement.vue new file mode 100644 index 0000000..ed5d1e1 --- /dev/null +++ b/frontend/src/components/system/DepartmentManagement.vue @@ -0,0 +1,1009 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/system/LLMConfigManagement.vue b/frontend/src/components/system/LLMConfigManagement.vue new file mode 100644 index 0000000..2f16bb6 --- /dev/null +++ b/frontend/src/components/system/LLMConfigManagement.vue @@ -0,0 +1,1909 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/system/RoleManagement.vue b/frontend/src/components/system/RoleManagement.vue new file mode 100644 index 0000000..48367bf --- /dev/null +++ b/frontend/src/components/system/RoleManagement.vue @@ -0,0 +1,1076 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/system/UserManagement.vue b/frontend/src/components/system/UserManagement.vue new file mode 100644 index 0000000..1d12b98 --- /dev/null +++ b/frontend/src/components/system/UserManagement.vue @@ -0,0 +1,1011 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/main.ts b/frontend/src/main.ts new file mode 100644 index 0000000..36100cc --- /dev/null +++ b/frontend/src/main.ts @@ -0,0 +1,23 @@ +import { createApp } from 'vue' +import { createPinia } from 'pinia' +import ElementPlus from 'element-plus' +import 'element-plus/dist/index.css' +import * as ElementPlusIconsVue from '@element-plus/icons-vue' +import 'nprogress/nprogress.css' + +import App from './App.vue' +import router from './router' +// import '@/styles/index.scss' + +const app = createApp(App) + +// Register Element Plus icons +for (const [key, component] of Object.entries(ElementPlusIconsVue)) { + app.component(key, component) +} + +app.use(createPinia()) +app.use(router) +app.use(ElementPlus) + +app.mount('#app') \ No newline at end of file diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts new file mode 100644 index 0000000..687843d --- /dev/null +++ b/frontend/src/router/index.ts @@ -0,0 +1,182 @@ +import { createRouter, createWebHistory } from 'vue-router' +import type { RouteRecordRaw } from 'vue-router' +import { useUserStore } from '@/stores/user' +import NProgress from 'nprogress' + +const routes: Array = [ + { + path: '/login', + name: 'Login', + component: () => import('../views/Login.vue'), + meta: { requiresAuth: false } + }, + { + path: '/register', + name: 'Register', + component: () => import('../views/Register.vue'), + meta: { requiresAuth: false } + }, + { + path: '/', + name: 'MainLayout', + component: () => import('../components/MainLayout.vue'), + meta: { requiresAuth: false }, + redirect: '/chat', + children: [ + { + path: 'chat', + name: 'Chat', + component: () => import('../views/Chat.vue'), + meta: { requiresAuth: false } + }, + { + path: 'knowledge', + name: 'Knowledge', + component: () => import('../components/KnowledgeManagement.vue'), + meta: { requiresAuth: false } + }, + { + path: 'workflow', + name: 'Workflow', + redirect: '/workflow/list', + meta: { requiresAuth: false }, + children: [ + { + path: 'list', + name: 'WorkflowList', + component: () => import('../views/Flow/WorkflowList.vue'), + meta: { requiresAuth: false } + }, + { + path: 'editor/:id?', + name: 'WorkflowEditor', + component: () => import('../components/WorkflowEditor.vue'), + meta: { requiresAuth: false } + } + ] + }, + { + path: 'agent', + name: 'Agent', + component: () => import('../components/AgentManagement.vue'), + meta: { requiresAuth: false } + }, + { + path: 'mcp-service', + name: 'MCPService', + component: () => import('../views/MCPServiceManagement.vue'), + meta: { requiresAuth: false } + }, + { + path: 'smart-query', + name: 'SmartQuery', + component: () => import('../components/SmartQuery.vue'), + meta: { requiresAuth: false } + }, + { + path: 'creation', + name: 'Creation', + component: () => import('../components/CreativeStudio.vue'), + meta: { requiresAuth: false } + }, + { + path: 'profile', + name: 'Profile', + component: () => import('../views/Profile.vue'), + meta: { requiresAuth: false } + }, + { + path: 'system', + name: 'System', + component: () => import('../views/SystemManagement.vue'), + meta: { requiresAuth: false, requiresAdmin: false }, + redirect: '/system/users', + children: [ + { + path: 'users', + name: 'SystemUsers', + component: () => import('../components/system/UserManagement.vue'), + meta: { requiresAuth: false, requiresAdmin: false } + }, + { + path: 'departments', + name: 'SystemDepartments', + component: () => import('../components/system/DepartmentManagement.vue'), + meta: { requiresAuth: false, requiresAdmin: false } + }, + { + path: 'roles', + name: 'SystemRoles', + component: () => import('../components/system/RoleManagement.vue'), + meta: { requiresAuth: false, requiresAdmin: false } + }, + + { + path: 'llm-configs', + name: 'SystemLLMConfigs', + component: () => import('../components/system/LLMConfigManagement.vue'), + meta: { requiresAuth: false, requiresAdmin: false } + } + ] + } + ] + }, + { + path: '/:pathMatch(.*)*', + name: 'NotFound', + component: () => import('../components/NotFound.vue') + } +] + +const router = createRouter({ + history: createWebHistory(), + routes +}) + +// Navigation guards +router.beforeEach(async (to, from, next) => { + NProgress.start() + + // Set page title + if (to.meta.title) { + document.title = `${to.meta.title} - TH-Agenter` + } + + const userStore = useUserStore() + + // Initialize user if token exists + if (!userStore.user && localStorage.getItem('access_token')) { + try { + await userStore.initializeUser() + } catch (error) { + console.log('Failed to initialize user') + } + } + + // Check authentication requirement + if (to.meta.requiresAuth && !userStore.isAuthenticated) { + next({ name: 'Login', query: { redirect: to.fullPath } }) + return + } + + // Check admin requirement + if (to.meta.requiresAdmin && !userStore.isAdmin) { + next({ name: 'Chat' }) + return + } + + // Redirect authenticated users away from login/register + // 但如果是因为token过期跳转过来的,则允许访问登录页面 + if ((to.name === 'Login' || to.name === 'Register') && userStore.isAuthenticated && !to.query.expired) { + next({ name: 'Chat' }) + return + } + + next() +}) + +router.afterEach(() => { + NProgress.done() +}) + +export default router \ No newline at end of file diff --git a/frontend/src/services/sse.ts b/frontend/src/services/sse.ts new file mode 100644 index 0000000..54a6786 --- /dev/null +++ b/frontend/src/services/sse.ts @@ -0,0 +1,290 @@ +/** + * SSE (Server-Sent Events) 服务,用于实时接收工作流执行状态 + */ + +export interface WorkflowExecutionMessage { + type: 'workflow_status' | 'node_status' | 'workflow_result' | 'error' + execution_id?: number + node_id?: string + status?: string + data?: any + timestamp?: string + message?: string +} + +export interface WorkflowExecutionCallbacks { + onWorkflowStarted?: (data: any) => void + onWorkflowCompleted?: (data: any) => void + onWorkflowFailed?: (data: any) => void + onNodeStarted?: (nodeId: string, data: any) => void + onNodeCompleted?: (nodeId: string, data: any) => void + onNodeFailed?: (nodeId: string, data: any) => void + onWorkflowResult?: (data: any) => void + onError?: (error: string) => void +} + +export class WorkflowSSEService { + private callbacks: WorkflowExecutionCallbacks = {} + private isConnected = false + private abortController: AbortController | null = null + private currentExecutionId: number | null = null + + /** + * 开始监听工作流执行 + */ + async startWorkflowExecution(workflowId: number, inputData: any = {}): Promise { + // 如果已经有连接,先关闭 + if (this.abortController) { + this.disconnect() + } + + try { + // 获取认证token + const token = localStorage.getItem('access_token') + if (!token) { + throw new Error('未找到认证token') + } + + // 执行工作流并处理流式响应 + await this.executeWorkflow(workflowId, inputData, token) + + } catch (error) { + console.error('启动工作流执行失败:', error) + this.callbacks.onError?.(error instanceof Error ? error.message : '启动工作流执行失败') + throw error + } + } + + /** + * 执行工作流 + */ + private async executeWorkflow(workflowId: number, inputData: any, token: string): Promise { + const baseUrl = import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000' + const url = `${baseUrl}/workflows/${workflowId}/execute-stream` + + // 创建AbortController来管理请求 + this.abortController = new AbortController() + + // 构建符合后端期望的数据结构 + const requestData = { + input_data: inputData + } + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + body: JSON.stringify(requestData), + signal: this.abortController.signal + }) + + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`) + } + + // 处理流式响应 + const reader = response.body?.getReader() + if (!reader) { + throw new Error('无法获取响应流') + } + + this.isConnected = true + this.processStream(reader) + } + + /** + * 处理流式响应 + */ + private async processStream(reader: ReadableStreamDefaultReader): Promise { + const decoder = new TextDecoder() + let buffer = '' + + try { + while (true) { + const { done, value } = await reader.read() + + if (done) { + break + } + + // 解码数据 + buffer += decoder.decode(value, { stream: true }) + + // 处理完整的消息 + const lines = buffer.split('\n') + buffer = lines.pop() || '' // 保留不完整的行 + + for (const line of lines) { + if (line.trim()) { + try { + // 解析SSE格式的数据 + if (line.startsWith('data: ')) { + const jsonData = line.substring(6) + const message: WorkflowExecutionMessage = JSON.parse(jsonData) + this.handleMessage(message) + } + } catch (error) { + console.error('解析SSE消息失败:', error, line) + } + } + } + } + } catch (error) { + console.error('处理SSE流失败:', error) + this.callbacks.onError?.(error instanceof Error ? error.message : '处理SSE流失败') + } finally { + this.isConnected = false + reader.releaseLock() + } + } + + + + /** + * 处理接收到的消息 + */ + private handleMessage(message: WorkflowExecutionMessage): void { + console.log('收到SSE消息:', message) + + switch (message.type) { + case 'workflow_start': + // 处理工作流开始消息 + this.callbacks.onWorkflowStarted?.(message) + break + case 'execution_update': + // 处理执行更新消息 + this.handleExecutionUpdate(message) + break + case 'workflow_status': + this.handleWorkflowStatus(message) + break + case 'node_status': + this.handleNodeStatus(message) + break + case 'workflow_result': + this.handleWorkflowResult(message) + break + case 'error': + this.callbacks.onError?.(message.message || '工作流执行错误') + break + default: + console.warn('未知的消息类型:', message.type) + } + } + + /** + * 处理工作流状态消息 + */ + private handleWorkflowStatus(message: WorkflowExecutionMessage): void { + if (message.execution_id) { + this.currentExecutionId = message.execution_id + } + + switch (message.status) { + case 'started': + this.callbacks.onWorkflowStarted?.(message.data) + break + case 'completed': + this.callbacks.onWorkflowCompleted?.(message.data) + break + case 'failed': + this.callbacks.onWorkflowFailed?.(message.data) + break + } + } + + /** + * 处理节点状态消息 + */ + private handleNodeStatus(message: WorkflowExecutionMessage): void { + if (!message.node_id) return + + switch (message.status) { + case 'started': + this.callbacks.onNodeStarted?.(message.node_id, message.data) + break + case 'completed': + this.callbacks.onNodeCompleted?.(message.node_id, message.data) + break + case 'failed': + this.callbacks.onNodeFailed?.(message.node_id, message.data) + break + } + } + + /** + * 处理工作流结果消息 + */ + private handleWorkflowResult(message: WorkflowExecutionMessage): void { + this.callbacks.onWorkflowResult?.(message.data) + } + + /** + * 处理执行更新消息 + */ + private handleExecutionUpdate(message: any): void { + const { update_type, data } = message + + switch (update_type) { + case 'node_status': + // 转换为node_status格式并处理 + const nodeMessage = { + type: 'node_status', + node_id: data.node_id, + status: data.status, + data: data.data + } + this.handleNodeStatus(nodeMessage) + break + case 'workflow_status': + // 转换为workflow_status格式并处理 + const workflowMessage = { + type: 'workflow_status', + status: data.status, + data: data.data + } + this.handleWorkflowStatus(workflowMessage) + break + default: + console.warn('未知的执行更新类型:', update_type) + } + } + + /** + * 设置回调函数 + */ + setCallbacks(callbacks: WorkflowExecutionCallbacks): void { + this.callbacks = { ...this.callbacks, ...callbacks } + } + + /** + * 断开连接 + */ + disconnect(): void { + if (this.abortController) { + this.abortController.abort() + this.abortController = null + } + this.isConnected = false + this.currentExecutionId = null + } + + /** + * 获取连接状态 + */ + isConnectedToWorkflow(): boolean { + return this.isConnected + } + + /** + * 获取当前执行ID + */ + getCurrentExecutionId(): number | null { + return this.currentExecutionId + } +} + +// 创建单例实例 +export const workflowSSEService = new WorkflowSSEService() \ No newline at end of file diff --git a/frontend/src/stores/chat.ts b/frontend/src/stores/chat.ts new file mode 100644 index 0000000..83becd9 --- /dev/null +++ b/frontend/src/stores/chat.ts @@ -0,0 +1,704 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { ElMessage } from 'element-plus' +import { chatApi } from '@/api' +import type { + Conversation, + ConversationCreate, + ConversationUpdate, + Message, + ChatRequest +} from '@/types' + +export const useChatStore = defineStore('chat', () => { + // State + const conversations = ref([]) + const currentConversation = ref(null) + const messages = ref([]) + const isLoading = ref(false) + const isStreaming = ref(false) + const isLoadingMessages = ref(false) + const searchQuery = ref('') + const includeArchived = ref(false) + const totalCount = ref(0) + const currentPage = ref(1) + const pageSize = ref(20) + + // Getters + const sortedConversations = computed(() => { + return [...conversations.value].sort((a, b) => + new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime() + ) + }) + + const activeConversations = computed(() => { + return conversations.value.filter(conv => !conv.is_archived) + }) + + const archivedConversations = computed(() => { + return conversations.value.filter(conv => conv.is_archived) + }) + + // Actions + const loadConversations = async (params?: { + search?: string + include_archived?: boolean + page?: number + limit?: number + order_by?: string + order_desc?: boolean + }) => { + try { + isLoading.value = true + const queryParams = { + search: params?.search || searchQuery.value, + include_archived: params?.include_archived ?? includeArchived.value, + skip: ((params?.page || currentPage.value) - 1) * pageSize.value, + limit: params?.limit || pageSize.value, + order_by: params?.order_by || 'updated_at', + order_desc: params?.order_desc ?? true + } + + const response = await chatApi.getConversations(queryParams) + conversations.value = response.data.data || response.data || [] + + // Load total count + const countResponse = await chatApi.getConversationsCount({ + search: queryParams.search, + include_archived: queryParams.include_archived + }) + totalCount.value = countResponse.data.count || countResponse.data.data?.count || 0 + } catch (error: any) { + console.error('Load conversations failed:', error) + ElMessage.error('加载对话列表失败') + } finally { + isLoading.value = false + } + } + + const createConversation = async (data: ConversationCreate) => { + try { + isLoading.value = true + console.log('Creating conversation with data:', data) + const response = await chatApi.createConversation(data) + console.log('Create conversation response:', response) + + const newConversation = response.data + console.log('New conversation:', newConversation) + + conversations.value.unshift(newConversation) + currentConversation.value = newConversation + messages.value = [] + + // ElMessage.success('创建对话成功') + return newConversation + } catch (error: any) { + console.error('Create conversation failed:', error) + console.error('Error response:', error.response?.data) + console.error('Error status:', error.response?.status) + ElMessage.error('创建对话失败: ' + (error.response?.data?.detail || error.message)) + return null + } finally { + isLoading.value = false + } + } + + const loadConversation = async (conversationId: string) => { + try { + isLoading.value = true + const response = await chatApi.getConversation(conversationId) + currentConversation.value = response.data.data || response.data + + // Update conversation in list if exists + const index = conversations.value.findIndex(conv => conv.id.toString() === conversationId) + if (index !== -1) { + conversations.value[index] = currentConversation.value + } + + return currentConversation.value + } catch (error: any) { + console.error('Load conversation failed:', error) + ElMessage.error('加载对话失败') + return null + } finally { + isLoading.value = false + } + } + + const updateConversation = async (conversationId: string, data: ConversationUpdate) => { + try { + const response = await chatApi.updateConversation(conversationId, data) + const updatedConversation = response.data.data || response.data + + // Update in list + const index = conversations.value.findIndex(conv => conv.id.toString() === conversationId) + if (index !== -1) { + conversations.value[index] = updatedConversation + } + + // Update current if it's the same + if (currentConversation.value?.id.toString() === conversationId) { + currentConversation.value = updatedConversation + } + + ElMessage.success('对话更新成功') + return updatedConversation + } catch (error: any) { + console.error('Update conversation failed:', error) + ElMessage.error('更新对话失败') + return null + } + } + + const deleteConversation = async (conversationId: string) => { + try { + await chatApi.deleteConversation(conversationId) + + // Remove from list + conversations.value = conversations.value.filter(conv => conv.id.toString() !== conversationId) + + // Clear current if it's the same + if (currentConversation.value?.id.toString() === conversationId) { + currentConversation.value = null + messages.value = [] + } + + ElMessage.success('对话删除成功') + return true + } catch (error: any) { + console.error('Delete conversation failed:', error) + ElMessage.error('删除对话失败') + return false + } + } + + const deleteAllConversations = async () => { + try { + await chatApi.deleteAllConversations() + + // Clear all conversations + conversations.value = [] + + // Clear current conversation and messages + currentConversation.value = null + messages.value = [] + + // ElMessage.success('所有对话已删除') + return true + } catch (error: any) { + console.error('Delete all conversations failed:', error) + ElMessage.error('删除所有对话失败') + return false + } + } + + const loadMessages = async (conversationId: string, forceReload = false) => { + try { + isLoadingMessages.value = true + const response = await chatApi.getMessages(conversationId) + const loadedMessages = response.data.data || response.data || [] + + // 只有在强制重新加载或消息为空时才替换消息数组 + console.log(forceReload) + if (forceReload || messages.value.length === 0) { + messages.value = loadedMessages + } + // messages.value = loadedMessages + return loadedMessages + } catch (error: any) { + console.error('Load messages failed:', error) + ElMessage.error('加载消息失败') + return [] + } finally { + isLoadingMessages.value = false + } + } + + const sendMessage = async (data: ChatRequest) => { + try { + console.log('sendMessage called with data:', data) + isStreaming.value = true + + // Add user message to local state immediately + const userMessage: Message = { + id: `temp-${Date.now()}`, + conversation_id: data.conversation_id || '', + role: 'user', + content: data.message, + message_type: 'text', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + } + messages.value.push(userMessage) + + console.log('Calling chatApi.sendMessage...') + // Convert conversation_id to string for API call + const conversationId = String(data.conversation_id!) + const response = await chatApi.sendMessage(conversationId, data) + console.log('API response:', response) + const { user_message, assistant_message } = response.data + + // Remove temp user message and add real messages + messages.value = messages.value.filter(msg => msg.id !== userMessage.id) + + // Add both user and assistant messages from response + messages.value.push(user_message) + messages.value.push(assistant_message) + + // Update current conversation if needed + if (currentConversation.value) { + const index = conversations.value.findIndex(conv => conv.id === currentConversation.value!.id) + if (index !== -1) { + // Update conversation in list + conversations.value[index] = { ...conversations.value[index], updated_at: new Date().toISOString() } + } + } + + return { user_message, assistant_message } + } catch (error: any) { + console.error('Send message failed:', error) + console.error('Error details:', error.response?.data || error.message) + ElMessage.error('发送消息失败: ' + (error.response?.data?.detail || error.message)) + + // Remove temp user message on error + messages.value = messages.value.filter(msg => !msg.id.startsWith('temp-')) + + return null + } finally { + isStreaming.value = false + } + } + + const sendMessageStream = async (data: ChatRequest, onChunk?: (chunk: string) => void) => { + try { + // 防止重复调用 + if (isStreaming.value) { + console.warn('Already streaming, ignoring duplicate call') + return + } + + isStreaming.value = true + + // Add user message immediately + const userMessage: Message = { + id: Date.now(), + conversation_id: data.conversation_id || 0, + role: 'user', + content: data.message, + message_type: 'text', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + } + messages.value.push(userMessage) + + // Add assistant message placeholder + const assistantMessage: Message = { + id: Date.now() + 1, + conversation_id: data.conversation_id || 0, + role: 'assistant', + content: '', + message_type: 'text', + created_at: new Date().toISOString(), + updated_at: new Date().toISOString() + } + messages.value.push(assistantMessage) + + // Convert conversation_id to string for API call + const conversationId = String(data.conversation_id!) + const stream = await chatApi.sendMessageStreamFetch(conversationId, data) + if (!stream) { + throw new Error('Failed to get stream') + } + + const reader = stream.getReader() + const decoder = new TextDecoder() + + try { + while (true) { + const { done, value } = await reader.read() + if (done) break + + const chunk = decoder.decode(value, { stream: true }) + const lines = chunk.split('\n') + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = line.slice(6).trim() + if (data === '[DONE]') { + break + } + + try { + const parsed = JSON.parse(data) + + // 处理智能体模式的流式响应 + const lastMessageIndex = messages.value.length - 1 + if (lastMessageIndex >= 0 && messages.value[lastMessageIndex].role === 'assistant') { + // 初始化智能体数据结构 + if (!messages.value[lastMessageIndex].agent_data) { + messages.value[lastMessageIndex].agent_data = { + status: 'thinking', + steps: [], + current_tool: null + } + } + + const agentData = messages.value[lastMessageIndex].agent_data + + if (parsed.type === 'thinking') { + // 处理思考过程 + console.log('🤔 Thinking:', parsed.content) + agentData.status = 'thinking' + agentData.steps.push({ + id: `thinking_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + type: 'thinking', + content: parsed.content, + timestamp: new Date().toISOString(), + node_name: parsed.node_name || 'agent', + raw_output: parsed.raw_output + }) + + } else if (parsed.type === 'tools_end') { + // 处理工具执行完成 + console.log('🔧 Tools end:', parsed.content) + agentData.status = 'tool_calling' + agentData.steps.push({ + id: `tools_end_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + type: 'tools_end', + content: parsed.content, + timestamp: new Date().toISOString(), + node_name: parsed.node_name || 'tools', + tool_name: parsed.tool_name, + tool_output: parsed.tool_output + }) + + } else if (parsed.type === 'response_start') { + // 处理开始输出 + console.log('💬 Response start:', parsed.content) + agentData.status = 'responding' + agentData.steps.push({ + id: `response_start_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + type: 'response_start', + content: '正在输出', + timestamp: new Date().toISOString() + }) + + } else if (parsed.type === 'response') { + // 处理流式响应内容,同时更新消息内容和steps中的response步骤 + if (!messages.value[lastMessageIndex].content) { + messages.value[lastMessageIndex].content = '' + } + messages.value[lastMessageIndex].content = parsed.content + + // 在agent_data.steps中创建或更新response步骤 + const steps = agentData.steps + const existingResponseIndex = steps.findIndex(step => step.type === 'response') + + const responseStep = { + id: `response_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + type: 'response', + content: parsed.content, + timestamp: new Date().toISOString() + } + + if (existingResponseIndex >= 0) { + // 更新已存在的response步骤内容 + steps[existingResponseIndex] = { + ...steps[existingResponseIndex], + content: parsed.content + } + } else { + // 添加新的response步骤 + steps.push(responseStep) + } + + } else if (parsed.type === 'complete') { + // 处理完成状态 + console.log('✅ Complete:', parsed.content) + agentData.status = 'completed' + + // 更新最后一个response_start步骤为complete + const lastResponseStartIndex = agentData.steps.findLastIndex(step => step.type === 'response_start') + if (lastResponseStartIndex >= 0) { + agentData.steps[lastResponseStartIndex] = { + ...agentData.steps[lastResponseStartIndex], + type: 'complete', + content: '本次对话过程完成' + } + } else { + agentData.steps.push({ + id: `complete_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + type: 'complete', + content: '本次对话过程完成', + timestamp: new Date().toISOString() + }) + } + + // 设置最终消息内容 + messages.value[lastMessageIndex].content = parsed.content + + } else if (parsed.type === 'status') { + // 处理状态消息 + console.log('📊 Status:', parsed.content) + messages.value[lastMessageIndex].status = parsed.content + } + } + + // 处理其他类型的流式数据 + if (parsed.type === 'content') { + // 处理内容块 + const lastMessageIndex = messages.value.length - 1 + if (lastMessageIndex >= 0 && messages.value[lastMessageIndex].role === 'assistant') { + messages.value[lastMessageIndex].content += parsed.content + } + } else if (parsed.type === 'tool' || parsed.type === 'tool_result') { + // 工具执行相关消息 + console.log('Tool execution:', parsed.content) + const lastMessageIndex = messages.value.length - 1 + if (lastMessageIndex >= 0 && messages.value[lastMessageIndex].role === 'assistant') { + // 显示工具执行状态 + messages.value[lastMessageIndex].status = parsed.content + } + } else if (parsed.type === 'content') { + // 流式内容块,累积到消息内容 + const lastMessageIndex = messages.value.length - 1 + if (lastMessageIndex >= 0 && messages.value[lastMessageIndex].role === 'assistant') { + if (!messages.value[lastMessageIndex].content) { + messages.value[lastMessageIndex].content = '' + } + messages.value[lastMessageIndex].content += parsed.content + // 清除状态显示 + messages.value[lastMessageIndex].status = undefined + + // 更新智能体状态为生成中 + if (messages.value[lastMessageIndex].agent_data) { + messages.value[lastMessageIndex].agent_data.status = 'generating' + } + } + onChunk?.(parsed.content) + } else if (parsed.type === 'response' && parsed.content) { + // 处理流式响应内容 + const lastMessageIndex = messages.value.length - 1 + if (lastMessageIndex >= 0 && messages.value[lastMessageIndex].role === 'assistant') { + if (parsed.done) { + // 完成时设置最终内容 + messages.value[lastMessageIndex].content = parsed.content + // 清除状态显示 + messages.value[lastMessageIndex].status = undefined + + // 更新智能体状态为完成 + if (messages.value[lastMessageIndex].agent_data) { + messages.value[lastMessageIndex].agent_data.status = 'completed' + + // 移除已存在的response步骤,确保只有一个最终response + const steps = messages.value[lastMessageIndex].agent_data.steps + const existingResponseIndex = steps.findIndex(step => step.type === 'response') + + const responseStep = { + id: `response_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + type: 'response', + content: parsed.content, + timestamp: new Date().toISOString() + } + + if (existingResponseIndex >= 0) { + // 替换已存在的response步骤 + steps[existingResponseIndex] = responseStep + } else { + // 添加新的response步骤 + steps.push(responseStep) + } + } + + // 兼容旧的思考过程数据 + if (messages.value[lastMessageIndex].thinking_data) { + messages.value[lastMessageIndex].thinking_data.status = 'completed' + messages.value[lastMessageIndex].thinking_data.current_step = '思考完成' + } + + onChunk?.(parsed.content) + } else { + // 流式过程中,累积更新content实现打字机效果 + messages.value[lastMessageIndex].content = parsed.content + + // 更新智能体状态为生成中 + if (messages.value[lastMessageIndex].agent_data) { + messages.value[lastMessageIndex].agent_data.status = 'responding' + + // 在流式过程中也要创建/更新response步骤 + const steps = messages.value[lastMessageIndex].agent_data.steps + const existingResponseIndex = steps.findIndex(step => step.type === 'response') + + const responseStep = { + id: `response_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`, + type: 'response', + content: parsed.content, + timestamp: new Date().toISOString() + } + + if (existingResponseIndex >= 0) { + // 更新已存在的response步骤内容 + steps[existingResponseIndex] = { + ...steps[existingResponseIndex], + content: parsed.content + } + } else { + // 添加新的response步骤 + steps.push(responseStep) + } + } + + onChunk?.(parsed.content) + } + } + } else if (parsed.content) { + // 兼容旧格式的流式响应 + const lastMessageIndex = messages.value.length - 1 + if (lastMessageIndex >= 0 && messages.value[lastMessageIndex].role === 'assistant') { + if (!messages.value[lastMessageIndex].content) { + messages.value[lastMessageIndex].content = '' + } + messages.value[lastMessageIndex].content += parsed.content + } + onChunk?.(parsed.content) + } + + // 检查是否流式传输完成 + if (parsed.done === true || parsed.finish_reason) { + // 流式传输完成 + break + } + } catch (e) { + console.warn('Failed to parse SSE data:', data) + } + } + } + } + } finally { + reader.releaseLock() + } + + } catch (error: any) { + console.error('Stream message failed:', error) + ElMessage.error('发送消息失败: ' + (error.response?.data?.detail || error.message)) + + // Remove temp messages on error + messages.value = messages.value.filter(msg => msg.id < Date.now() - 1000) + } finally { + isStreaming.value = false + } + } + + const setCurrentConversation = async (conversationId: number) => { + try { + const conversation = await loadConversation(conversationId.toString()) + if (conversation) { + // 只有在消息为空且切换到不同对话时才加载消息 + // 如果当前有消息且是同一个对话,不要重新加载以保持消息连续性 + const shouldLoadMessages = messages.value.length === 0 && + (!currentConversation.value || currentConversation.value.id !== conversationId) + + if (shouldLoadMessages) { + await loadMessages(conversationId.toString()) + } + } + return conversation + } catch (error) { + console.error('Set current conversation failed:', error) + return null + } + } + + const clearCurrentConversation = () => { + currentConversation.value = null + messages.value = [] + } + + const clearMessages = () => { + messages.value = [] + } + + const addMessage = (message: Message) => { + messages.value.push(message) + } + + const archiveConversation = async (conversationId: string) => { + try { + await chatApi.archiveConversation(conversationId) + const conversation = conversations.value.find(c => c.id.toString() === conversationId) + if (conversation) { + conversation.is_archived = true + } + ElMessage.success('对话已归档') + } catch (error: any) { + console.error('Archive conversation failed:', error) + ElMessage.error('归档对话失败') + } + } + + const unarchiveConversation = async (conversationId: string) => { + try { + await chatApi.unarchiveConversation(conversationId) + const conversation = conversations.value.find(c => c.id.toString() === conversationId) + if (conversation) { + conversation.is_archived = false + } + ElMessage.success('对话已取消归档') + } catch (error: any) { + console.error('Unarchive conversation failed:', error) + ElMessage.error('取消归档对话失败') + } + } + + const setSearchQuery = (query: string) => { + searchQuery.value = query + } + + const setIncludeArchived = (include: boolean) => { + includeArchived.value = include + } + + const setCurrentPage = (page: number) => { + currentPage.value = page + } + + return { + // State + conversations, + currentConversation, + messages, + isLoading, + isStreaming, + isLoadingMessages, + searchQuery, + includeArchived, + totalCount, + currentPage, + pageSize, + + // Getters + sortedConversations, + activeConversations, + archivedConversations, + + // Actions + loadConversations, + createConversation, + loadConversation, + updateConversation, + deleteConversation, + deleteAllConversations, + loadMessages, + sendMessage, + sendMessageStream, + setCurrentConversation, + clearCurrentConversation, + clearMessages, + addMessage, + archiveConversation, + unarchiveConversation, + setSearchQuery, + setIncludeArchived, + setCurrentPage + } +}) \ No newline at end of file diff --git a/frontend/src/stores/index.ts b/frontend/src/stores/index.ts new file mode 100644 index 0000000..ec1b93d --- /dev/null +++ b/frontend/src/stores/index.ts @@ -0,0 +1,4 @@ +// Export all stores +export { useUserStore } from './user' +export { useChatStore } from './chat' +export { useKnowledgeStore } from './knowledge' \ No newline at end of file diff --git a/frontend/src/stores/knowledge.ts b/frontend/src/stores/knowledge.ts new file mode 100644 index 0000000..b0ae8af --- /dev/null +++ b/frontend/src/stores/knowledge.ts @@ -0,0 +1,312 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { ElMessage } from 'element-plus' +import { knowledgeApi } from '@/api' +import type { + KnowledgeBase, + KnowledgeBaseCreate, + KnowledgeBaseUpdate, + Document, + SearchRequest, + SearchResult +} from '@/types' + +export const useKnowledgeStore = defineStore('knowledge', () => { + // State + const knowledgeBases = ref([]) + const currentKnowledgeBase = ref(null) + const documents = ref([]) + const searchResults = ref([]) + const isLoading = ref(false) + const isLoadingDocuments = ref(false) + const isUploading = ref(false) + const isSearching = ref(false) + + // Getters + const activeKnowledgeBases = computed(() => { + return knowledgeBases.value.filter(kb => kb.is_active) + }) + + const sortedKnowledgeBases = computed(() => { + return [...knowledgeBases.value].sort((a, b) => + new Date(b.updated_at).getTime() - new Date(a.updated_at).getTime() + ) + }) + + const documentsByStatus = computed(() => { + const grouped = { + pending: [] as Document[], + processing: [] as Document[], + completed: [] as Document[], + failed: [] as Document[] + } + + documents.value.forEach(doc => { + if (doc.status in grouped) { + grouped[doc.status as keyof typeof grouped].push(doc) + } + }) + + return grouped + }) + + // Actions + const loadKnowledgeBases = async () => { + try { + isLoading.value = true + const response = await knowledgeApi.getKnowledgeBases() + knowledgeBases.value = response.data || [] + } catch (error: any) { + console.error('Load knowledge bases failed:', error) + ElMessage.error('加载知识库列表失败') + } finally { + isLoading.value = false + } + } + + const createKnowledgeBase = async (data: KnowledgeBaseCreate) => { + try { + isLoading.value = true + const response = await knowledgeApi.createKnowledgeBase(data) + const newKnowledgeBase = response.data + + knowledgeBases.value.unshift(newKnowledgeBase) + currentKnowledgeBase.value = newKnowledgeBase + + ElMessage.success('创建知识库成功') + return newKnowledgeBase + } catch (error: any) { + console.error('Create knowledge base failed:', error) + ElMessage.error('创建知识库失败') + return null + } finally { + isLoading.value = false + } + } + + const loadKnowledgeBase = async (knowledgeBaseId: string) => { + try { + isLoading.value = true + const response = await knowledgeApi.getKnowledgeBase(knowledgeBaseId) + currentKnowledgeBase.value = response.data + + // Update in list if exists + const index = knowledgeBases.value.findIndex(kb => kb.id.toString() === knowledgeBaseId) + if (index !== -1) { + knowledgeBases.value[index] = currentKnowledgeBase.value + } + + return currentKnowledgeBase.value + } catch (error: any) { + console.error('Load knowledge base failed:', error) + ElMessage.error('加载知识库失败') + return null + } finally { + isLoading.value = false + } + } + + const updateKnowledgeBase = async (knowledgeBaseId: string, data: KnowledgeBaseUpdate) => { + try { + const response = await knowledgeApi.updateKnowledgeBase(knowledgeBaseId, data) + const updatedKnowledgeBase = response.data + + // Update in list + const index = knowledgeBases.value.findIndex(kb => kb.id.toString() === knowledgeBaseId) + if (index !== -1) { + knowledgeBases.value[index] = updatedKnowledgeBase + } + + // Update current if it's the same + if (currentKnowledgeBase.value?.id.toString() === knowledgeBaseId) { + currentKnowledgeBase.value = updatedKnowledgeBase + } + + ElMessage.success('知识库更新成功') + return updatedKnowledgeBase + } catch (error: any) { + console.error('Update knowledge base failed:', error) + ElMessage.error('更新知识库失败') + return null + } + } + + const deleteKnowledgeBase = async (knowledgeBaseId: string) => { + try { + await knowledgeApi.deleteKnowledgeBase(knowledgeBaseId) + + // Remove from list + knowledgeBases.value = knowledgeBases.value.filter(kb => kb.id.toString() !== knowledgeBaseId) + + // Clear current if it's the same + if (currentKnowledgeBase.value?.id.toString() === knowledgeBaseId) { + currentKnowledgeBase.value = null + documents.value = [] + } + + ElMessage.success('知识库删除成功') + return true + } catch (error: any) { + console.error('Delete knowledge base failed:', error) + ElMessage.error('删除知识库失败') + return false + } + } + + const loadDocuments = async (knowledgeBaseId: string) => { + try { + isLoadingDocuments.value = true + const response = await knowledgeApi.getDocuments(knowledgeBaseId) + // 后端返回的是 DocumentListResponse 格式: {documents: [], total, page, page_size} + const documentList = response.data?.documents || response.data || [] + documents.value = documentList + return documents.value + } catch (error: any) { + console.error('Load documents failed:', error) + ElMessage.error('加载文档列表失败') + return [] + } finally { + isLoadingDocuments.value = false + } + } + + const uploadDocument = async (knowledgeBaseId: string, file: File) => { + try { + isUploading.value = true + const response = await knowledgeApi.uploadDocument(knowledgeBaseId, file) + const newDocument = response.data + + documents.value.unshift(newDocument) + + ElMessage.success('文档上传成功') + return newDocument + } catch (error: any) { + console.error('Upload document failed:', error) + ElMessage.error('文档上传失败') + return null + } finally { + isUploading.value = false + } + } + + const deleteDocument = async (knowledgeBaseId: string, documentId: string) => { + try { + await knowledgeApi.deleteDocument(knowledgeBaseId, documentId) + + // Remove from list + documents.value = documents.value.filter(doc => doc.id.toString() !== documentId) + + ElMessage.success('文档删除成功') + return true + } catch (error: any) { + console.error('Delete document failed:', error) + ElMessage.error('删除文档失败') + return false + } + } + + const processDocument = async (knowledgeBaseId: string, documentId: string) => { + try { + const response = await knowledgeApi.processDocument(knowledgeBaseId, documentId) + const updatedDocument = response.data + + // Update in list + const index = documents.value.findIndex(doc => doc.id.toString() === documentId) + if (index !== -1) { + documents.value[index] = updatedDocument + } + + ElMessage.success('文档处理已开始') + return updatedDocument + } catch (error: any) { + console.error('Process document failed:', error) + ElMessage.error('文档处理失败') + return null + } + } + + const searchKnowledgeBase = async (data: SearchRequest) => { + try { + isSearching.value = true + const response = await knowledgeApi.searchKnowledgeBase(data) + searchResults.value = response.data || [] + return searchResults.value + } catch (error: any) { + console.error('Search knowledge base failed:', error) + ElMessage.error('搜索失败') + return [] + } finally { + isSearching.value = false + } + } + + const clearSearchResults = () => { + searchResults.value = [] + } + + const clearCurrentKnowledgeBase = () => { + currentKnowledgeBase.value = null + documents.value = [] + searchResults.value = [] + } + + const getDocumentChunks = async (documentId: string) => { + try { + // 从当前文档列表中找到对应的知识库ID + const document = documents.value.find(doc => doc.id.toString() === documentId) + if (!document || !document.knowledge_base_id) { + throw new Error('Document not found or missing knowledge base ID') + } + + const response = await knowledgeApi.getDocumentChunks(document.knowledge_base_id.toString(), documentId) + return response.data?.chunks || response.data || [] + } catch (error: any) { + console.error('Get document chunks failed:', error) + ElMessage.error('获取文档分段失败') + return [] + } + } + + const updateDocumentStatus = (documentId: string, status: Document['status'], chunks?: number) => { + const index = documents.value.findIndex(doc => doc.id.toString() === documentId) + if (index !== -1) { + documents.value[index].status = status + if (chunks !== undefined) { + documents.value[index].chunks = chunks + } + } + } + + return { + // State + knowledgeBases, + currentKnowledgeBase, + documents, + searchResults, + isLoading, + isLoadingDocuments, + isUploading, + isSearching, + + // Getters + activeKnowledgeBases, + sortedKnowledgeBases, + documentsByStatus, + + // Actions + loadKnowledgeBases, + createKnowledgeBase, + loadKnowledgeBase, + updateKnowledgeBase, + deleteKnowledgeBase, + loadDocuments, + uploadDocument, + deleteDocument, + processDocument, + getDocumentChunks, + searchKnowledgeBase, + clearSearchResults, + clearCurrentKnowledgeBase, + updateDocumentStatus + } +}) \ No newline at end of file diff --git a/frontend/src/stores/menu.ts b/frontend/src/stores/menu.ts new file mode 100644 index 0000000..17ef010 --- /dev/null +++ b/frontend/src/stores/menu.ts @@ -0,0 +1,113 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { useUserStore } from './user' + +export interface MenuItem { + key: string + label: string + icon: string + route: string + expandable?: boolean + children?: MenuItem[] + requires_admin?: boolean +} + +export const useMenuStore = defineStore('menu', () => { + // State + const userStore = useUserStore() + + // Getters + const upperNavItems = computed(() => { + const items: MenuItem[] = [ + { + key: 'chat', + label: '智能问答', + icon: 'ChatDotRound', + route: '/chat' + }, + { + key: 'smart-query', + label: '智能问数', + icon: 'DataAnalysis', + route: '/smart-query' + }, + { + key: 'creation', + label: '智能创作', + icon: 'EditPen', + route: '/creation' + } + ] + + return items + }) + + const lowerNavItems = computed(() => { + const items: MenuItem[] = [ + { + key: 'knowledge', + label: '知识库', + icon: 'Collection', + route: '/knowledge' + }, + { + key: 'workflow', + label: '工作流编排', + icon: 'Connection', + route: '/workflow' + }, + { + key: 'agent', + label: '智能体管理', + icon: 'Cpu', + route: '/agent' + }, + { + key: 'mcp-service', + label: 'MCP服务管理', + icon: 'Link', + route: '/mcp-service' + }, + ] + + // 如果是超级管理员,添加系统管理菜单 + if (userStore.user?.is_superuser) { + items.push({ + key: 'system', + label: '系统管理', + icon: 'Setting', + route: '/system', + expandable: true, + children: [ + { + key: 'users', + label: '用户管理', + icon: 'User', + route: '/system/users' + }, + { + key: 'roles', + label: '角色管理', + icon: 'UserFilled', + route: '/system/roles' + }, + { + key: 'llm-configs', + label: '大模型管理', + icon: 'Cpu', + route: '/system/llm-configs' + } + ], + requires_admin: true + }) + } + + return items + }) + + return { + // Getters + upperNavItems, + lowerNavItems + } +}) \ No newline at end of file diff --git a/frontend/src/stores/user.ts b/frontend/src/stores/user.ts new file mode 100644 index 0000000..e41e31c --- /dev/null +++ b/frontend/src/stores/user.ts @@ -0,0 +1,241 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { ElMessage } from 'element-plus' +import { authApi, usersApi, rolesApi } from '@/api' +import type { User, UserLogin, UserCreate, UserUpdate, LoginRequest, Role } from '@/types' + +export const useUserStore = defineStore('user', () => { + // State + const user = ref(null) + const isLoading = ref(false) + + // Getters + const isAuthenticated = computed(() => { + // 主要检查token是否存在,用户信息可以稍后获取 + const hasToken = !!localStorage.getItem('access_token') + console.log('检查认证状态:', { hasToken, hasUser: !!user.value }) + return hasToken + }) + + const isAdmin = computed(() => { + if (!user.value) return false + + // Check if user has admin role + if (user.value.roles) { + return user.value.roles.some(role => + role.is_active && (role.code === 'SUPER_ADMIN' || role.code === 'ADMIN' || role.code === 'AAA') + ) + } + + return false + }) + + // Actions + const login = async (credentials: LoginRequest) => { + try { + isLoading.value = true + console.log('开始登录流程...') + + const response = await authApi.login(credentials) + const { access_token, token_type } = response.data + console.log('登录API调用成功,获取到token') + + // Store tokens + localStorage.setItem('access_token', access_token) + // Note: Backend doesn't return refresh_token yet, using access_token as placeholder + localStorage.setItem('refresh_token', access_token) + console.log('Token已保存到localStorage') + + // Get user info + try { + await getCurrentUser() + console.log('用户信息获取成功,登录流程完成') + return true + } catch (userError: any) { + console.error('获取用户信息失败,但登录token有效:', userError) + // 即使获取用户信息失败,如果token有效,仍然认为登录成功 + ElMessage.warning('登录成功,但获取用户信息失败,请刷新页面') + return true + } + } catch (error: any) { + console.error('Login failed:', error) + ElMessage.error(error.response?.data?.detail || error.message || '登录失败') + return false + } finally { + isLoading.value = false + } + } + + const register = async (userData: UserCreate) => { + try { + isLoading.value = true + const response = await authApi.register(userData) + + ElMessage.success('注册成功,请登录') + return true + } catch (error: any) { + console.error('Registration failed:', error) + ElMessage.error(error.response?.data?.detail || error.message || '注册失败') + return false + } finally { + isLoading.value = false + } + } + + const getCurrentUser = async () => { + try { + const response = await authApi.getCurrentUser() + user.value = response.data + + // Get user roles + if (user.value) { + await getUserRoles() + } + + return user.value + } catch (error: any) { + console.error('Get current user failed:', error) + + // Don't clear tokens on any error, let the user re-login to update tokens + if (error.response && error.response.status === 401) { + console.log('Authentication failed, but keeping tokens for re-login') + user.value = null + } else { + console.log('Non-authentication error, keeping tokens and user state') + } + + throw error + } + } + + const getUserRoles = async () => { + try { + if (!user.value) return + + const response = await rolesApi.getUserRoles(user.value.id) + if (user.value) { + user.value.roles = response.data + } + } catch (error: any) { + console.error('Get user roles failed:', error) + // Don't throw error, just log it as roles are optional + } + } + + const updateProfile = async (userData: UserUpdate) => { + try { + isLoading.value = true + const response = await usersApi.updateProfile(userData) + user.value = response.data + + ElMessage.success('个人资料更新成功') + return true + } catch (error: any) { + console.error('Update profile failed:', error) + ElMessage.error(error.response?.data?.detail || error.message || '更新失败') + return false + } finally { + isLoading.value = false + } + } + + const changePassword = async (passwordData: { current_password: string; new_password: string }) => { + try { + isLoading.value = true + await usersApi.changePassword(passwordData) + + ElMessage.success('密码修改成功') + return true + } catch (error: any) { + console.error('Change password failed:', error) + // API拦截器已经处理了错误消息显示,这里不需要重复显示 + throw error // 重新抛出错误,让调用方知道操作失败 + } finally { + isLoading.value = false + } + } + + const logout = async () => { + try { + await authApi.logout() + user.value = null + ElMessage.success('已退出登录') + } catch (error) { + console.error('Logout error:', error) + } + } + + const refreshToken = async () => { + try { + const refreshToken = localStorage.getItem('refresh_token') + if (!refreshToken) { + throw new Error('No refresh token available') + } + + const response = await authApi.refreshToken(refreshToken) + const { access_token, token_type } = response.data + + localStorage.setItem('access_token', access_token) + // Note: Backend doesn't return refresh_token yet, using access_token as placeholder + localStorage.setItem('refresh_token', access_token) + + return true + } catch (error) { + console.error('Token refresh failed:', error) + await logout() + return false + } + } + + const deleteAccount = async () => { + try { + isLoading.value = true + await usersApi.deleteAccount() + await logout() + + ElMessage.success('账户已删除') + return true + } catch (error: any) { + console.error('Delete account failed:', error) + ElMessage.error(error.message || '删除账户失败') + return false + } finally { + isLoading.value = false + } + } + + // Initialize user on store creation + const initializeUser = async () => { + const token = localStorage.getItem('access_token') + if (token && !user.value) { + try { + await getCurrentUser() + } catch (error) { + // Token is invalid, will be cleared in getCurrentUser + console.log('Failed to initialize user with stored token') + } + } + } + + return { + // State + user, + isLoading, + + // Getters + isAuthenticated, + isAdmin, + + // Actions + login, + register, + getCurrentUser, + getUserRoles, + updateProfile, + changePassword, + logout, + refreshToken, + deleteAccount, + initializeUser + } +}) \ No newline at end of file diff --git a/frontend/src/styles/index.scss b/frontend/src/styles/index.scss new file mode 100644 index 0000000..6aa4f5f --- /dev/null +++ b/frontend/src/styles/index.scss @@ -0,0 +1,229 @@ +// Global styles for openAgent + +// Custom CSS variables +:root { + // Colors + --chat-primary-color: #409eff; + --chat-success-color: #67c23a; + --chat-warning-color: #e6a23c; + --chat-danger-color: #f56c6c; + --chat-info-color: #909399; + + // Chat specific colors + --chat-bg-color: #f5f7fa; + --chat-sidebar-bg: #ffffff; + --chat-message-user-bg: #409eff; + --chat-message-assistant-bg: #f0f2f5; + --chat-message-system-bg: #e1f3d8; + + // Spacing + --chat-padding-sm: 8px; + --chat-padding-md: 16px; + --chat-padding-lg: 24px; + --chat-padding-xl: 32px; + + // Border radius + --chat-border-radius-sm: 4px; + --chat-border-radius-md: 8px; + --chat-border-radius-lg: 12px; + + // Shadows + --chat-shadow-light: 0 2px 4px rgba(0, 0, 0, 0.1); + --chat-shadow-medium: 0 4px 8px rgba(0, 0, 0, 0.15); + --chat-shadow-heavy: 0 8px 16px rgba(0, 0, 0, 0.2); + + // Transitions + --chat-transition-fast: 0.2s ease; + --chat-transition-normal: 0.3s ease; + --chat-transition-slow: 0.5s ease; +} + +// Reset and base styles +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html, body { + height: 100%; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; + background-color: var(--chat-bg-color); + line-height: 1.6; +} + +#app { + height: 100vh; + overflow: hidden; +} + +// Chat specific styles +.chat-layout { + height: 100vh; + display: flex; + overflow: hidden; +} + +.chat-sidebar { + width: 280px; + background: var(--chat-sidebar-bg); + border-right: 1px solid #e4e7ed; + display: flex; + flex-direction: column; +} + +.chat-main { + flex: 1; + display: flex; + flex-direction: column; + overflow: hidden; +} + +.chat-messages { + flex: 1; + overflow-y: auto; + padding: var(--chat-padding-md); + background: var(--chat-bg-color); +} + +.chat-input { + padding: var(--chat-padding-md); + border-top: 1px solid #e4e7ed; + background: #ffffff; +} + +.message-item { + margin-bottom: var(--chat-padding-md); + display: flex; + align-items: flex-start; + gap: var(--chat-padding-sm); +} + +.message-item.user { + flex-direction: row-reverse; +} + +.message-content { + max-width: 70%; + padding: var(--chat-padding-sm) var(--chat-padding-md); + border-radius: var(--chat-border-radius-lg); + word-wrap: break-word; + line-height: 1.5; +} + +.message-content.user { + background: var(--chat-message-user-bg); + color: white; +} + +.message-content.assistant { + background: var(--chat-message-assistant-bg); + color: #303133; +} + +.message-content.system { + background: var(--chat-message-system-bg); + color: #303133; + font-style: italic; +} + +// Input styles +.el-input { + &.el-input--prefix, + &.el-input--suffix { + .el-input__inner { + padding-left: 36px; + padding-right: 36px; + } + } + + &.el-input--prefix { + .el-input__inner { + padding-left: 36px; + } + } + + &.el-input--suffix { + .el-input__inner { + padding-right: 36px; + } + } + + // Search input styles with non-white background + .el-input__inner { + background-color: #f0f2f5; // Use the same background as assistant messages + border: 1px solid #dcdfe6; + transition: all 0.3s ease; + + &:focus { + background-color: #ffffff; + border-color: var(--chat-primary-color); + box-shadow: 0 0 0 2px rgba(64, 158, 255, 0.2); + } + } +} + +// Utility classes +.flex { + display: flex; +} + +.flex-col { + flex-direction: column; +} + +.items-center { + align-items: center; +} + +.justify-between { + justify-content: space-between; +} + +.flex-1 { + flex: 1; +} + +.w-full { + width: 100%; +} + +.h-full { + height: 100%; +} + +.overflow-hidden { + overflow: hidden; +} + +.overflow-y-auto { + overflow-y: auto; +} + +.text-center { + text-align: center; +} + +.cursor-pointer { + cursor: pointer; +} + +// Responsive design +@media (max-width: 768px) { + .chat-sidebar { + width: 100%; + position: absolute; + z-index: 1000; + height: 100%; + transform: translateX(-100%); + transition: transform 0.3s ease; + } + + .chat-sidebar.open { + transform: translateX(0); + } + + .message-content { + max-width: 85%; + } +} \ No newline at end of file diff --git a/frontend/src/types/index.ts b/frontend/src/types/index.ts new file mode 100644 index 0000000..dcd933a --- /dev/null +++ b/frontend/src/types/index.ts @@ -0,0 +1,440 @@ +// API Response Types +export interface ApiResponse { + success: boolean + data?: T + message?: string + error?: string +} + +// User Types +export interface User { + id: number + username: string + email: string + full_name?: string + is_active: boolean + is_admin: boolean + department_id?: number + avatar_url?: string + bio?: string + created_at: string + updated_at: string + roles?: Role[] +} + +// Role Types +export interface Role { + id: number + name: string + code: string + description?: string + is_active: boolean + permissions?: Permission[] +} + +export interface Permission { + id: number + name: string + code: string + description?: string + category: string +} + +// Department Types +export interface Department { + id: number + name: string + code: string + description?: string + parent_id?: number + manager_id?: number + is_active: boolean + created_at: string + updated_at: string + manager?: { + id: number + username: string + full_name?: string + } + children?: Department[] + user_count?: number +} + +export interface UserCreate { + username: string + email: string + password: string + full_name?: string +} + +export interface UserUpdate { + username?: string + email?: string + full_name?: string + bio?: string + avatar_url?: string +} + +export interface UserLogin { + username: string + password: string +} + +export interface LoginRequest { + email: string + password: string +} + +export interface AuthTokens { + access_token: string + refresh_token: string + token_type: string +} + +// Conversation Types +export interface Conversation { + id: number + title: string + user_id: number + knowledge_base_id?: number + system_prompt?: string + model_name: string + temperature: string + max_tokens: number + is_archived: boolean + created_at: string + updated_at: string + message_count?: number + last_message_at?: string +} + +export interface ConversationCreate { + title: string + knowledge_base_id?: number + system_prompt?: string + model_name?: string + temperature?: string + max_tokens?: number +} + +export interface ConversationUpdate { + title?: string + knowledge_base_id?: number + system_prompt?: string + model_name?: string + temperature?: string + max_tokens?: number + is_archived?: boolean +} + +// Message Types +export type MessageRole = 'user' | 'assistant' | 'system' +export type MessageType = 'text' | 'image' | 'file' | 'audio' + +export interface Message { + id: number + conversation_id: number + role: MessageRole + content: string + message_type: MessageType + metadata?: Record + context_documents?: string[] + prompt_tokens?: number + completion_tokens?: number + total_tokens?: number + created_at: string + updated_at: string + status?: string // 用于显示流式状态,如"正在思考..."、"正在使用工具..."等 + thinking_data?: ThinkingData // LangGraph智能体思考过程数据 +} + +// LangGraph思考过程数据结构 +export interface ThinkingData { + steps: ThinkingStep[] + tool_calls: ToolCall[] + current_step: string + status: 'thinking' | 'tool_calling' | 'completed' | 'error' +} + +export interface ThinkingStep { + id: number + type: 'thinking' | 'tool_start' | 'tool_end' + content: string + timestamp: string + node_name?: string + tool_call?: ToolCall +} + +export interface ToolCall { + id: number + name: string + input: Record + output: any + status: 'running' | 'completed' | 'error' + timestamp: string +} + +export interface MessageCreate { + content: string + message_type?: MessageType + metadata?: Record +} + +export interface ChatRequest { + message: string + conversation_id?: number + stream?: boolean + use_knowledge_base?: boolean + knowledge_base_id?: number + use_agent?: boolean + use_langgraph?: boolean + temperature?: number + max_tokens?: number +} + +export interface ChatResponse { + user_message: Message + assistant_message: Message + total_tokens?: number + model_used: string +} + +// Knowledge Base Types +export interface KnowledgeBase { + id: number + name: string + description?: string + embedding_model: string + chunk_size: number + chunk_overlap: number + is_active: boolean + vector_db_type: string + collection_name: string + created_at: string + updated_at: string + document_count?: number +} + +export interface KnowledgeBaseCreate { + name: string + description?: string + embedding_model?: string + chunk_size?: number + chunk_overlap?: number +} + +export interface KnowledgeBaseUpdate { + name?: string + description?: string + embedding_model?: string + chunk_size?: number + chunk_overlap?: number + is_active?: boolean +} + +// Document Types +export type DocumentStatus = 'pending' | 'processing' | 'completed' | 'failed' + +export interface Document { + id: number + knowledge_base_id: number + filename: string + original_filename: string + file_path: string + file_size: number + file_type: string + mime_type: string + status: DocumentStatus + processing_error?: string + content?: string + metadata?: Record + chunk_count?: number + embedding_info?: Record + created_at: string + updated_at: string +} + +export interface DocumentUpload { + file: File + knowledge_base_id: number +} + +export interface DocumentListResponse { + documents: Document[] + total: number + page: number + page_size: number +} + +// Document Chunk Types +export interface DocumentChunk { + id: string + content: string + metadata: Record + page_number?: number + chunk_index: number + start_char?: number + end_char?: number +} + +export interface DocumentChunksResponse { + document_id: number + document_name: string + total_chunks: number + chunks: DocumentChunk[] +} + +export interface SearchRequest { + query: string + knowledge_base_id: number + top_k?: number + score_threshold?: number +} + +export interface SearchResult { + content: string + metadata: Record + score: number + document_id: number +} + +// UI State Types +export interface ChatState { + conversations: Conversation[] + currentConversation: Conversation | null + messages: Message[] + isLoading: boolean + isStreaming: boolean +} + +export interface KnowledgeState { + knowledgeBases: KnowledgeBase[] + currentKnowledgeBase: KnowledgeBase | null + documents: Document[] + isLoading: boolean +} + +export interface UserState { + user: User | null + isAuthenticated: boolean + isLoading: boolean +} + +// Form Types +export interface LoginForm { + username: string + password: string + remember: boolean +} + +export interface RegisterForm { + username: string + email: string + password: string + confirmPassword: string + fullName: string +} + +export interface ProfileForm { + username: string + email: string + fullName: string + bio: string +} + +// Component Props Types +export interface MessageItemProps { + message: Message + isStreaming?: boolean +} + +export interface ConversationItemProps { + conversation: Conversation + isActive?: boolean +} + +export interface KnowledgeBaseItemProps { + knowledgeBase: KnowledgeBase + isActive?: boolean +} + +export interface DocumentItemProps { + document: Document +} + +// Workflow Types +export interface NodeParameter { + name: string + type: 'string' | 'number' | 'boolean' | 'object' | 'array' + description?: string + required: boolean + default_value?: any + source?: 'input' | 'node' | 'variable' + source_node_id?: string + source_field?: string +} + +export interface NodeInputOutput { + inputs: NodeParameter[] + outputs: NodeParameter[] +} + +export interface WorkflowNode { + id: string + type: string + name: string + description?: string + x: number + y: number + config: Record + parameters?: NodeInputOutput + result?: { + success: boolean + data?: any + error?: string + } +} + +export interface WorkflowConnection { + id: string + from: string + to: string + fromPoint: string + toPoint: string +} + +export interface Workflow { + id: number + name: string + description?: string + definition: { + nodes: WorkflowNode[] + connections: WorkflowConnection[] + } + status: string + is_active: boolean + created_at: string + updated_at: string +} + +// Utility Types +export type LoadingState = 'idle' | 'loading' | 'success' | 'error' + +export interface PaginationParams { + page?: number + size?: number + skip?: number + limit?: number +} + +export interface SortParams { + sort_by?: string + sort_order?: 'asc' | 'desc' +} + +export interface FilterParams { + search?: string + status?: string + is_active?: boolean + [key: string]: any +} \ No newline at end of file diff --git a/frontend/src/utils/date.ts b/frontend/src/utils/date.ts new file mode 100644 index 0000000..f38dc62 --- /dev/null +++ b/frontend/src/utils/date.ts @@ -0,0 +1,72 @@ +/** + * 日期时间工具函数 + */ + +/** + * 格式化日期时间 + * @param date 日期字符串或Date对象 + * @param format 格式化模板,默认为 'YYYY-MM-DD HH:mm:ss' + * @returns 格式化后的日期字符串 + */ +export function formatDateTime(date: string | Date | null | undefined, format: string = 'YYYY-MM-DD HH:mm:ss'): string { + if (!date) return '-' + + const d = typeof date === 'string' ? new Date(date) : date + + if (isNaN(d.getTime())) return '-' + + const year = d.getFullYear() + const month = String(d.getMonth() + 1).padStart(2, '0') + const day = String(d.getDate()).padStart(2, '0') + const hours = String(d.getHours()).padStart(2, '0') + const minutes = String(d.getMinutes()).padStart(2, '0') + const seconds = String(d.getSeconds()).padStart(2, '0') + + return format + .replace('YYYY', String(year)) + .replace('MM', month) + .replace('DD', day) + .replace('HH', hours) + .replace('mm', minutes) + .replace('ss', seconds) +} + +/** + * 格式化日期(不包含时间) + * @param date 日期字符串或Date对象 + * @returns 格式化后的日期字符串 + */ +export function formatDate(date: string | Date | null | undefined): string { + return formatDateTime(date, 'YYYY-MM-DD') +} + +/** + * 格式化时间(不包含日期) + * @param date 日期字符串或Date对象 + * @returns 格式化后的时间字符串 + */ +export function formatTime(date: string | Date | null | undefined): string { + return formatDateTime(date, 'HH:mm:ss') +} + +/** + * 获取相对时间描述 + * @param date 日期字符串或Date对象 + * @returns 相对时间描述,如"刚刚"、"5分钟前"等 + */ +export function getRelativeTime(date: string | Date | null | undefined): string { + if (!date) return '-' + + const d = typeof date === 'string' ? new Date(date) : date + if (isNaN(d.getTime())) return '-' + + const now = new Date() + const diff = now.getTime() - d.getTime() + + if (diff < 60000) return '刚刚' + if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前` + if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前` + if (diff < 2592000000) return `${Math.floor(diff / 86400000)}天前` + + return formatDate(date) +} \ No newline at end of file diff --git a/frontend/src/utils/index.ts b/frontend/src/utils/index.ts new file mode 100644 index 0000000..2024a88 --- /dev/null +++ b/frontend/src/utils/index.ts @@ -0,0 +1,315 @@ +// Utility functions for TH-Agenter + +/** + * Format time to readable string + */ +export const formatTime = (date: string | Date): string => { + const d = new Date(date) + return d.toLocaleTimeString('zh-CN', { + hour: '2-digit', + minute: '2-digit' + }) +} + +/** + * Format date to readable string + */ +export const formatDate = (date: string | Date, format: 'full' | 'date' | 'time' | 'relative' = 'relative'): string => { + const d = new Date(date) + const now = new Date() + const diff = now.getTime() - d.getTime() + + if (format === 'relative') { + const seconds = Math.floor(diff / 1000) + const minutes = Math.floor(seconds / 60) + const hours = Math.floor(minutes / 60) + const days = Math.floor(hours / 24) + + if (seconds < 60) { + return '刚刚' + } else if (minutes < 60) { + return `${minutes}分钟前` + } else if (hours < 24) { + return `${hours}小时前` + } else if (days < 7) { + return `${days}天前` + } else { + return d.toLocaleDateString('zh-CN') + } + } + + const options: Intl.DateTimeFormatOptions = { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + second: '2-digit' + } + + switch (format) { + case 'full': + return d.toLocaleString('zh-CN', options) + case 'date': + return d.toLocaleDateString('zh-CN') + case 'time': + return d.toLocaleTimeString('zh-CN') + default: + return d.toLocaleString('zh-CN', options) + } +} + +/** + * Format file size to readable string + */ +export const formatFileSize = (bytes: number): string => { + if (bytes === 0) return '0 B' + + const k = 1024 + const sizes = ['B', 'KB', 'MB', 'GB', 'TB'] + const i = Math.floor(Math.log(bytes) / Math.log(k)) + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i] +} + +/** + * Debounce function + */ +export const debounce = any>( + func: T, + wait: number +): ((...args: Parameters) => void) => { + let timeout: NodeJS.Timeout + + return (...args: Parameters) => { + clearTimeout(timeout) + timeout = setTimeout(() => func.apply(null, args), wait) + } +} + +/** + * Throttle function + */ +export const throttle = any>( + func: T, + limit: number +): ((...args: Parameters) => void) => { + let inThrottle: boolean + + return (...args: Parameters) => { + if (!inThrottle) { + func.apply(null, args) + inThrottle = true + setTimeout(() => inThrottle = false, limit) + } + } +} + +/** + * Generate random ID + */ +export const generateId = (length: number = 8): string => { + const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789' + let result = '' + for (let i = 0; i < length; i++) { + result += chars.charAt(Math.floor(Math.random() * chars.length)) + } + return result +} + +/** + * Copy text to clipboard + */ +export const copyToClipboard = async (text: string): Promise => { + try { + if (navigator.clipboard && window.isSecureContext) { + await navigator.clipboard.writeText(text) + return true + } else { + // Fallback for older browsers + const textArea = document.createElement('textarea') + textArea.value = text + textArea.style.position = 'fixed' + textArea.style.left = '-999999px' + textArea.style.top = '-999999px' + document.body.appendChild(textArea) + textArea.focus() + textArea.select() + + const result = document.execCommand('copy') + document.body.removeChild(textArea) + return result + } + } catch (error) { + console.error('Failed to copy text:', error) + return false + } +} + +/** + * Validate email format + */ +export const isValidEmail = (email: string): boolean => { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + return emailRegex.test(email) +} + +/** + * Validate password strength + */ +export const validatePassword = (password: string): { + isValid: boolean + errors: string[] +} => { + const errors: string[] = [] + + if (password.length < 8) { + errors.push('密码长度至少8位') + } + + if (!/[A-Z]/.test(password)) { + errors.push('密码必须包含至少一个大写字母') + } + + if (!/[a-z]/.test(password)) { + errors.push('密码必须包含至少一个小写字母') + } + + if (!/\d/.test(password)) { + errors.push('密码必须包含至少一个数字') + } + + if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) { + errors.push('密码必须包含至少一个特殊字符') + } + + return { + isValid: errors.length === 0, + errors + } +} + +/** + * Truncate text with ellipsis + */ +export const truncateText = (text: string, maxLength: number): string => { + if (text.length <= maxLength) { + return text + } + return text.slice(0, maxLength) + '...' +} + +/** + * Get file extension from filename + */ +export const getFileExtension = (filename: string): string => { + return filename.slice((filename.lastIndexOf('.') - 1 >>> 0) + 2) +} + +/** + * Check if file type is supported + */ +export const isSupportedFileType = (filename: string, supportedTypes: string[]): boolean => { + const extension = getFileExtension(filename).toLowerCase() + return supportedTypes.includes(extension) +} + +/** + * Parse error message from API response + */ +export const parseErrorMessage = (error: any): string => { + if (typeof error === 'string') { + return error + } + + if (error?.response?.data?.message) { + return error.response.data.message + } + + if (error?.response?.data?.error) { + return error.response.data.error + } + + if (error?.response?.data?.detail) { + if (Array.isArray(error.response.data.detail)) { + return error.response.data.detail[0]?.msg || '请求参数错误' + } + return error.response.data.detail + } + + if (error?.message) { + return error.message + } + + return '未知错误' +} + +/** + * Sleep function for delays + */ +export const sleep = (ms: number): Promise => { + return new Promise(resolve => setTimeout(resolve, ms)) +} + +/** + * Check if running in development mode + */ +export const isDevelopment = (): boolean => { + return import.meta.env.MODE === 'development' +} + +/** + * Get API base URL + */ +export const getApiBaseUrl = (): string => { + return import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000/api' +} + +/** + * Format conversation title + */ +export const formatConversationTitle = (title: string, maxLength: number = 30): string => { + if (!title || title.trim() === '') { + return '新对话' + } + return truncateText(title.trim(), maxLength) +} + +/** + * Get message type icon + */ +export const getMessageTypeIcon = (type: string): string => { + const icons: Record = { + text: 'ChatDotRound', + image: 'Picture', + file: 'Document', + audio: 'Microphone' + } + return icons[type] || 'ChatDotRound' +} + +/** + * Get document status color + */ +export const getDocumentStatusColor = (status: string): string => { + const colors: Record = { + pending: 'warning', + processing: 'primary', + completed: 'success', + failed: 'danger' + } + return colors[status] || 'info' +} + +/** + * Get document status text + */ +export const getDocumentStatusText = (status: string): string => { + const texts: Record = { + pending: '待处理', + processing: '处理中', + completed: '已完成', + failed: '处理失败' + } + return texts[status] || '未知状态' +} \ No newline at end of file diff --git a/frontend/src/views/Chat.vue b/frontend/src/views/Chat.vue new file mode 100644 index 0000000..542efaf --- /dev/null +++ b/frontend/src/views/Chat.vue @@ -0,0 +1,1903 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Flow/WorkflowList.vue b/frontend/src/views/Flow/WorkflowList.vue new file mode 100644 index 0000000..e18a6c3 --- /dev/null +++ b/frontend/src/views/Flow/WorkflowList.vue @@ -0,0 +1,563 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Knowledge.vue b/frontend/src/views/Knowledge.vue new file mode 100644 index 0000000..3be2b4b --- /dev/null +++ b/frontend/src/views/Knowledge.vue @@ -0,0 +1,724 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue new file mode 100644 index 0000000..919a3f2 --- /dev/null +++ b/frontend/src/views/Login.vue @@ -0,0 +1,184 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/MCPServiceManagement.vue b/frontend/src/views/MCPServiceManagement.vue new file mode 100644 index 0000000..c5c9df1 --- /dev/null +++ b/frontend/src/views/MCPServiceManagement.vue @@ -0,0 +1,676 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Profile.vue b/frontend/src/views/Profile.vue new file mode 100644 index 0000000..e03982e --- /dev/null +++ b/frontend/src/views/Profile.vue @@ -0,0 +1,374 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/Register.vue b/frontend/src/views/Register.vue new file mode 100644 index 0000000..159652d --- /dev/null +++ b/frontend/src/views/Register.vue @@ -0,0 +1,203 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/views/SystemManagement.vue b/frontend/src/views/SystemManagement.vue new file mode 100644 index 0000000..1de667f --- /dev/null +++ b/frontend/src/views/SystemManagement.vue @@ -0,0 +1,56 @@ + + + + + \ No newline at end of file diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000..cac055b --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,25 @@ +{ + "extends": "@vue/tsconfig/tsconfig.dom.json", + "include": [ + "env.d.ts", + "src/**/*", + "src/**/*.vue" + ], + "exclude": [ + "src/**/__tests__/*" + ], + "compilerOptions": { + "composite": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + }, + "types": [ + "vite/client", + "element-plus/global" + ], + "strict": true, + "noUnusedLocals": false, + "noUnusedParameters": false + } +} \ No newline at end of file diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts new file mode 100644 index 0000000..b10a208 --- /dev/null +++ b/frontend/vite.config.ts @@ -0,0 +1,85 @@ +import { defineConfig, loadEnv } from 'vite' +import vue from '@vitejs/plugin-vue' +import { resolve } from 'path' +import AutoImport from 'unplugin-auto-import/vite' +import Components from 'unplugin-vue-components/vite' +import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' + +// https://vitejs.dev/config/ +export default defineConfig(({ mode }) => { + const env = loadEnv(mode, process.cwd(), '') + const base = process.argv.includes('--base') + ? process.argv[process.argv.indexOf('--base') + 1] + : '/'; // 默认值 + return { + base, + plugins: [ + vue(), + AutoImport({ + resolvers: [ElementPlusResolver()], + imports: [ + 'vue', + 'vue-router', + 'pinia', + { + axios: [ + ['default', 'axios'] + ] + } + ], + dts: true + }), + Components({ + resolvers: [ElementPlusResolver()] + }) + ], + resolve: { + alias: { + '@': resolve(__dirname, 'src') + } + }, + server: { + port: 3000, + host: '0.0.0.0', + proxy: { + '/api': { + target: 'http://127.0.0.1:8000', + changeOrigin: true, + secure: false, + rewrite: (path) => path, + configure: (proxy, options) => { + proxy.on('proxyReq', (proxyReq, req, res) => { + // 确保Authorization头部被正确传递 + if (req.headers.authorization) { + proxyReq.setHeader('Authorization', req.headers.authorization); + } + }); + } + } + } + }, + build: { + outDir: 'dist', + sourcemap: false, + chunkSizeWarningLimit: 1600, + rollupOptions: { + output: { + manualChunks: { + 'vue-vendor': ['vue', 'vue-router', 'pinia', 'element-plus'], + } + } + } + }, + css: { + preprocessorOptions: { + scss: { + additionalData: `@import "@/styles/variables.scss";"` + } + } + }, + // 定义环境变量 + define: { + __VITE_ENV__: JSON.stringify(env) + } + } +}) \ No newline at end of file