botrun-flow-lang 5.12.264__tar.gz → 6.2.21__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/CHANGELOG.md +12 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/PKG-INFO +6 -6
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/langgraph_api.py +8 -3
- botrun_flow_lang-6.2.21/botrun_flow_lang/api/langgraph_constants.py +11 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/langgraph_react_agent.py +11 -4
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/img_util.py +58 -16
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/pdf_analyzer.py +112 -36
- botrun_flow_lang-6.2.21/botrun_flow_lang/langgraph_agents/agents/util/usage_metadata.py +34 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/mcp_server/default_mcp.py +152 -42
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/pyproject.toml +88 -87
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/uv.lock +3377 -3090
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/.env_template +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/.gcloudignore +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/.gitignore +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/.vscode/launch.json +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/CLAUDE.md +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/Dockerfile.template +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/README.md +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/__init__.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/__init__.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/auth_api.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/auth_utils.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/botrun_back_api.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/flow_api.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/hatch_api.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/line_bot_api.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/model_api.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/rate_limit_api.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/routes.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/search_api.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/storage_api.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/subsidy_api.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/subsidy_api_system_prompt.txt +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/user_setting_api.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/version_api.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/youtube_api.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/constants.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/__init__.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/__init__.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/agent_runner.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/agent_tools/__init__.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/agent_tools/step_planner.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/checkpointer/__init__.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/checkpointer/firestore_checkpointer.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/gov_researcher/GOV_RESEARCHER_PRD.md +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/gov_researcher/__init__.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/gov_researcher/gemini_subsidy_graph.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_2_graph.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/gov_researcher/gov_researcher_graph.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/search_agent_graph.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/tools/__init__.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/tools/gemini_code_execution.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/__init__.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/gemini_grounding.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/html_util.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/local_files.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/mermaid_util.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/model_utils.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/pdf_cache.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/pdf_processor.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/perplexity_search.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/plotly_util.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/tavily_search.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/youtube_util.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/cache/__init__.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/cache/langgraph_botrun_cache.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/llm_agent/__init__.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/llm_agent/llm_agent.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/llm_agent/llm_agent_util.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/log/.gitignore +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/main.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/main_fast.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/mcp_server/__init__.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/models/__init__.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/models/nodes/utils.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/models/token_usage.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/requirements.txt +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/__init__.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/base/firestore_base.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/hatch/__init__.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/hatch/hatch_factory.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/hatch/hatch_fs_store.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/storage/__init__.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/storage/storage_cs_store.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/storage/storage_factory.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/storage/storage_store.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/user_setting/__init__.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/user_setting/user_setting_factory.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/user_setting/user_setting_fs_store.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/static/docs/tools/index.html +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/api_functional_tests.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/api_stress_test.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/shared_hatch_tests.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_botrun_app.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_files/(/346/272/253/351/246/250/346/210/220/346/236/234 /350/241/214/346/224/277/350/253/213/347/244/272/345/214/257/347/270/275)20250210/345/220/221 /344/270/212/344/272/272/345/240/261/345/221/212/347/260/241/345/240/261 (1).pdf" +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_files/11206_10808/344/272/272/345/217/243/346/225/270(3/346/256/265/345/271/264/351/275/241/347/265/204+/346/257/224/347/216/207)/345/244/251/344/270/213/351/233/234/350/252/2141.pdf" +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_files/1120701A/346/265/267/345/273/243/351/233/242/345/262/270/351/242/250/345/212/233/347/231/274/351/233/273/350/250/210/347/225/253/347/222/260/345/242/203/345/275/261/351/237/277/350/252/252/346/230/216/346/233/270-C04.PDF" +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_files/ImportedPhoto.760363950.029251.jpeg +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_files/ImportedPhoto.760363950.030446.jpeg +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_files/ImportedPhoto.760363950.031127.jpeg +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_files/d5712343.jpg +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_files/spot_difference_1.png +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_files/spot_difference_2.png +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_html_util.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_img_analyzer.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_img_util.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_local_files.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_mermaid_util.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_pdf_analyzer.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_plotly_util.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_run_workflow_engine.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tools/generate_docs.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tools/templates/tools.html +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/utils/__init__.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/utils/botrun_logger.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/utils/clients/__init__.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/utils/clients/rate_limit_client.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/utils/clients/token_verify_client.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/utils/google_drive_utils.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/utils/langchain_utils.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/utils/yaml_utils.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/cloudbuild_template.yaml +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/convert_newlines.sh +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/deploy.sh +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/langgraph.json +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/poetry.lock +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/poetry.toml +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/requirements.txt +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/requirements_fast.txt +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/deploy_bigline.sh +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/deploy_dev.sh +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/deploy_evaldev.sh +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/deploy_fast.sh +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/deploy_modaline.sh +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/deploy_prod.sh +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/deploy_sebaline.sh +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/firestore_checkpointer_delete_thread.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/generate_docs.sh +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/llm_time_test.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/run_langgraph.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/run_langgraph_react.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/run_youtube_summary.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/scrape_pdf.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/seba_pypi.sh +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/subsidy_test.py +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/specs/gov-search/agent-architecture-upgrade.excalidraw +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/specs/gov-search/line_dev.excalidraw +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/specs/gov-search/subsidy_agent.excalidraw +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/specs/gov-search/subsidy_agent_dev.excalidraw +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/specs/gov-search/temp_txt.md +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/specs/gov-search/temp_txt2.md +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/specs/gov-search//346/264/245/350/262/274line/345/276/205/350/250/216/350/253/226.excalidraw" +0 -0
- {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/specs/system_thinking.excalidraw +0 -0
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
## [6.1.261]
|
|
2
|
+
- 優化 MCP 工具 docstring,避免 LLM 混淆 URL 有效期限:
|
|
3
|
+
- `create_html_page`:明確標示為 PERMANENT URL that never expires
|
|
4
|
+
- `generate_tmp_public_url`:改為 temporary URL that may be deleted periodically
|
|
5
|
+
|
|
6
|
+
## [6.1.72]
|
|
7
|
+
- web_search 如果沒有使用 openrouter 的 api 時,回傳的 usage_metadata model 要加入 perplexity/ 前綴
|
|
8
|
+
|
|
9
|
+
## [6.1.71]
|
|
10
|
+
- 幾個 mcp 工具加入回傳 llm usage_metadata
|
|
11
|
+
- web_search、chat_with_pdf、chat_with_imgs、generate_image
|
|
12
|
+
|
|
1
13
|
## [5.12.264]
|
|
2
14
|
- 修正 botrun_flow_lang/langgraph_agents/agents/langgraph_react_agent.py
|
|
3
15
|
- from langchain.tools import StructuredTool 改成 from langchain_core.tools import StructuredTool
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: botrun-flow-lang
|
|
3
|
-
Version:
|
|
3
|
+
Version: 6.2.21
|
|
4
4
|
Summary: A flow language for botrun
|
|
5
5
|
Author-email: sebastian-hsu <sebastian.hsu@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -27,12 +27,12 @@ Requires-Dist: google-cloud-storage<3,>=2.18
|
|
|
27
27
|
Requires-Dist: google-genai>=1.28.0
|
|
28
28
|
Requires-Dist: jinja2>=3.1.6
|
|
29
29
|
Requires-Dist: langchain-anthropic>=0.3.10
|
|
30
|
-
Requires-Dist: langchain-aws>=0.
|
|
30
|
+
Requires-Dist: langchain-aws>=1.0.0
|
|
31
31
|
Requires-Dist: langchain-community>=0.3.27
|
|
32
|
-
Requires-Dist: langchain-core>=
|
|
32
|
+
Requires-Dist: langchain-core>=1.1.2
|
|
33
33
|
Requires-Dist: langchain-google-community>=2.0.3
|
|
34
|
-
Requires-Dist: langchain-google-genai>=
|
|
35
|
-
Requires-Dist: langchain-google-vertexai<
|
|
34
|
+
Requires-Dist: langchain-google-genai>=4.0.0
|
|
35
|
+
Requires-Dist: langchain-google-vertexai<4.0.0,>=3.2.0
|
|
36
36
|
Requires-Dist: langchain-mcp-adapters>=0.1.7
|
|
37
37
|
Requires-Dist: langchain-openai>=0.3.28
|
|
38
38
|
Requires-Dist: langchain>=0.3.27
|
|
@@ -41,7 +41,7 @@ Requires-Dist: langgraph-supervisor>=0.0.20
|
|
|
41
41
|
Requires-Dist: langgraph>=0.6.3
|
|
42
42
|
Requires-Dist: line-bot-sdk>=3.17.1
|
|
43
43
|
Requires-Dist: mcp<1.11.0,>=1.10.1
|
|
44
|
-
Requires-Dist: numpy
|
|
44
|
+
Requires-Dist: numpy>=1.24.0
|
|
45
45
|
Requires-Dist: openai>=1.99.1
|
|
46
46
|
Requires-Dist: pandas>=2.2.3
|
|
47
47
|
Requires-Dist: pdfminer-six==20250506
|
|
@@ -113,12 +113,17 @@ class GraphSchemaRequest(BaseModel):
|
|
|
113
113
|
graph_name: str
|
|
114
114
|
|
|
115
115
|
|
|
116
|
-
|
|
116
|
+
# 從常數檔案匯入,避免外部模組為了取得常數而觸發重型 import
|
|
117
|
+
from botrun_flow_lang.api.langgraph_constants import (
|
|
118
|
+
LANGGRAPH_REACT_AGENT,
|
|
119
|
+
GOV_SUBSIDY_AGENT,
|
|
120
|
+
PERPLEXITY_SEARCH_AGENT,
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
# 僅在此檔案內部使用的常數
|
|
117
124
|
CUSTOM_WEB_RESEARCH_AGENT = "custom_web_research_agent"
|
|
118
|
-
LANGGRAPH_REACT_AGENT = "langgraph_react_agent"
|
|
119
125
|
DEEP_RESEARCH_AGENT = "deep_research_agent"
|
|
120
126
|
# GOV_RESEARCHER_AGENT = "gov_researcher_agent"
|
|
121
|
-
GOV_SUBSIDY_AGENT = "gov_subsidy_agent"
|
|
122
127
|
GEMINI_SUBSIDY_AGENT = "gemini_subsidy_agent"
|
|
123
128
|
|
|
124
129
|
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LangGraph 常數定義
|
|
3
|
+
|
|
4
|
+
此檔案只包含常數定義,不包含任何會觸發重型 SDK 載入的 import。
|
|
5
|
+
這樣可以讓其他模組在只需要常數時,不會觸發 langchain_google_vertexai 等重型套件的載入。
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
# Graph 名稱常數
|
|
9
|
+
LANGGRAPH_REACT_AGENT = "langgraph_react_agent"
|
|
10
|
+
GOV_SUBSIDY_AGENT = "gov_subsidy_agent"
|
|
11
|
+
PERPLEXITY_SEARCH_AGENT = "perplexity_search_agent"
|
|
@@ -86,8 +86,9 @@ from langchain_mcp_adapters.client import MultiServerMCPClient
|
|
|
86
86
|
# ========
|
|
87
87
|
# for Vertex AI
|
|
88
88
|
from google.oauth2 import service_account
|
|
89
|
-
|
|
90
|
-
|
|
89
|
+
# 重型 import 改為延遲載入,避免啟動時載入 google-cloud-aiplatform(約 26 秒)
|
|
90
|
+
# ChatVertexAI 已遷移至 ChatGoogleGenerativeAI(vertexai=True)
|
|
91
|
+
# ChatAnthropicVertex 在需要時才 import(見 get_react_agent_model 函數內)
|
|
91
92
|
|
|
92
93
|
load_dotenv()
|
|
93
94
|
|
|
@@ -234,8 +235,10 @@ def get_react_agent_model(model_name: str = ""):
|
|
|
234
235
|
# 判斷模型類型並創建相應實例
|
|
235
236
|
if vertex_model_name.startswith("gemini-"):
|
|
236
237
|
# Gemini 系列:gemini-2.5-pro, gemini-2.5-flash, gemini-pro
|
|
237
|
-
|
|
238
|
+
# 使用 ChatGoogleGenerativeAI + vertexai=True,避免載入重型的 langchain_google_vertexai
|
|
239
|
+
model = ChatGoogleGenerativeAI(
|
|
238
240
|
model=vertex_model_name,
|
|
241
|
+
vertexai=True,
|
|
239
242
|
location=vertex_region,
|
|
240
243
|
project=vertex_project,
|
|
241
244
|
credentials=credentials,
|
|
@@ -243,11 +246,13 @@ def get_react_agent_model(model_name: str = ""):
|
|
|
243
246
|
max_tokens=GEMINI_MAX_TOKENS,
|
|
244
247
|
)
|
|
245
248
|
logger.info(
|
|
246
|
-
f"model
|
|
249
|
+
f"model ChatGoogleGenerativeAI(vertexai=True) {vertex_model_name} @ {vertex_region} (project: {vertex_project})"
|
|
247
250
|
)
|
|
248
251
|
|
|
249
252
|
elif "claude" in vertex_model_name.lower() or vertex_model_name.startswith("maison/"):
|
|
250
253
|
# Anthropic Claude (model garden)
|
|
254
|
+
# 延遲載入 ChatAnthropicVertex,只有在需要時才觸發 langchain_google_vertexai
|
|
255
|
+
from langchain_google_vertexai.model_garden import ChatAnthropicVertex
|
|
251
256
|
model = ChatAnthropicVertex(
|
|
252
257
|
model=vertex_model_name,
|
|
253
258
|
location=vertex_region,
|
|
@@ -302,6 +307,8 @@ def get_react_agent_model(model_name: str = ""):
|
|
|
302
307
|
)
|
|
303
308
|
|
|
304
309
|
# 初始化 ChatAnthropicVertex
|
|
310
|
+
# 延遲載入,只有在需要時才觸發 langchain_google_vertexai
|
|
311
|
+
from langchain_google_vertexai.model_garden import ChatAnthropicVertex
|
|
305
312
|
model = ChatAnthropicVertex(
|
|
306
313
|
project=vertex_project,
|
|
307
314
|
model=vertex_model,
|
|
@@ -4,8 +4,11 @@ import httpx
|
|
|
4
4
|
import os
|
|
5
5
|
import imghdr
|
|
6
6
|
from pathlib import Path
|
|
7
|
+
from typing import Dict, Any, List, Tuple
|
|
7
8
|
from dotenv import load_dotenv
|
|
8
9
|
|
|
10
|
+
from botrun_flow_lang.langgraph_agents.agents.util.usage_metadata import UsageMetadata
|
|
11
|
+
|
|
9
12
|
load_dotenv()
|
|
10
13
|
|
|
11
14
|
|
|
@@ -50,7 +53,7 @@ def get_img_content_type(file_path: str | Path) -> str:
|
|
|
50
53
|
|
|
51
54
|
def analyze_imgs_with_claude(
|
|
52
55
|
img_urls: list[str], user_input: str, model_name: str = "claude-sonnet-4-5-20250929"
|
|
53
|
-
) -> str:
|
|
56
|
+
) -> Tuple[str, UsageMetadata]:
|
|
54
57
|
"""
|
|
55
58
|
Analyze multiple images using Claude Vision API
|
|
56
59
|
|
|
@@ -60,7 +63,7 @@ def analyze_imgs_with_claude(
|
|
|
60
63
|
model_name: Claude model name to use
|
|
61
64
|
|
|
62
65
|
Returns:
|
|
63
|
-
str: Claude's analysis
|
|
66
|
+
Tuple[str, UsageMetadata]: Claude's analysis and usage metadata
|
|
64
67
|
|
|
65
68
|
Raises:
|
|
66
69
|
ValueError: If image URLs are invalid or model parameters are incorrect
|
|
@@ -120,10 +123,20 @@ def analyze_imgs_with_claude(
|
|
|
120
123
|
],
|
|
121
124
|
)
|
|
122
125
|
|
|
126
|
+
# Extract usage metadata
|
|
127
|
+
usage = UsageMetadata(
|
|
128
|
+
prompt_tokens=message.usage.input_tokens,
|
|
129
|
+
completion_tokens=message.usage.output_tokens,
|
|
130
|
+
total_tokens=message.usage.input_tokens + message.usage.output_tokens,
|
|
131
|
+
cache_creation_input_tokens=getattr(message.usage, 'cache_creation_input_tokens', 0) or 0,
|
|
132
|
+
cache_read_input_tokens=getattr(message.usage, 'cache_read_input_tokens', 0) or 0,
|
|
133
|
+
model=model_name,
|
|
134
|
+
)
|
|
135
|
+
|
|
123
136
|
print(
|
|
124
137
|
f"analyze_imgs_with_claude============> input_token: {message.usage.input_tokens} output_token: {message.usage.output_tokens}",
|
|
125
138
|
)
|
|
126
|
-
return message.content[0].text
|
|
139
|
+
return message.content[0].text, usage
|
|
127
140
|
except anthropic.APIError as e:
|
|
128
141
|
import traceback
|
|
129
142
|
|
|
@@ -144,7 +157,7 @@ def analyze_imgs_with_gemini(
|
|
|
144
157
|
img_urls: list[str],
|
|
145
158
|
user_input: str,
|
|
146
159
|
model_name: str = "gemini-2.5-flash",
|
|
147
|
-
) -> str:
|
|
160
|
+
) -> Tuple[str, UsageMetadata]:
|
|
148
161
|
"""
|
|
149
162
|
Analyze multiple images using Gemini Vision API
|
|
150
163
|
|
|
@@ -154,7 +167,7 @@ def analyze_imgs_with_gemini(
|
|
|
154
167
|
model_name: Gemini model name to use
|
|
155
168
|
|
|
156
169
|
Returns:
|
|
157
|
-
str: Gemini's analysis
|
|
170
|
+
Tuple[str, UsageMetadata]: Gemini's analysis and usage metadata
|
|
158
171
|
|
|
159
172
|
Raises:
|
|
160
173
|
ValueError: If image URLs are invalid or model parameters are incorrect
|
|
@@ -216,10 +229,23 @@ def analyze_imgs_with_gemini(
|
|
|
216
229
|
contents=contents,
|
|
217
230
|
)
|
|
218
231
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
)
|
|
222
|
-
|
|
232
|
+
# Extract usage metadata
|
|
233
|
+
usage = UsageMetadata(model=model_name)
|
|
234
|
+
if hasattr(response, "usage_metadata"):
|
|
235
|
+
usage_meta = response.usage_metadata
|
|
236
|
+
usage = UsageMetadata(
|
|
237
|
+
prompt_tokens=getattr(usage_meta, 'prompt_token_count', 0) or 0,
|
|
238
|
+
completion_tokens=getattr(usage_meta, 'candidates_token_count', 0) or 0,
|
|
239
|
+
total_tokens=getattr(usage_meta, 'total_token_count', 0) or 0,
|
|
240
|
+
cache_creation_input_tokens=0,
|
|
241
|
+
cache_read_input_tokens=getattr(usage_meta, 'cached_content_token_count', 0) or 0,
|
|
242
|
+
model=model_name,
|
|
243
|
+
)
|
|
244
|
+
print(
|
|
245
|
+
f"analyze_imgs_with_gemini============> input_token: {usage_meta.prompt_token_count} output_token: {usage_meta.candidates_token_count}"
|
|
246
|
+
)
|
|
247
|
+
|
|
248
|
+
return response.text, usage
|
|
223
249
|
|
|
224
250
|
except httpx.RequestError as e:
|
|
225
251
|
import traceback
|
|
@@ -233,7 +259,7 @@ def analyze_imgs_with_gemini(
|
|
|
233
259
|
raise Exception(f"Error analyzing image(s) with Gemini {model_name}: {str(e)}")
|
|
234
260
|
|
|
235
261
|
|
|
236
|
-
def analyze_imgs(img_urls: list[str], user_input: str) -> str:
|
|
262
|
+
def analyze_imgs(img_urls: list[str], user_input: str) -> Dict[str, Any]:
|
|
237
263
|
"""
|
|
238
264
|
Analyze multiple images using configured AI models.
|
|
239
265
|
|
|
@@ -248,8 +274,13 @@ def analyze_imgs(img_urls: list[str], user_input: str) -> str:
|
|
|
248
274
|
user_input: User's query about the image content(s)
|
|
249
275
|
|
|
250
276
|
Returns:
|
|
251
|
-
str:
|
|
277
|
+
Dict[str, Any]: {
|
|
278
|
+
"result": str, # AI analysis result
|
|
279
|
+
"usage_metadata": List[Dict] # Token usage for each LLM call
|
|
280
|
+
}
|
|
252
281
|
"""
|
|
282
|
+
usage_list: List[UsageMetadata] = []
|
|
283
|
+
|
|
253
284
|
# Get models from environment variable, split by comma if multiple models
|
|
254
285
|
models_str = os.getenv("IMG_ANALYZER_MODEL", "gemini-2.5-flash")
|
|
255
286
|
print(f"[analyze_imgs] 分析IMG使用模型: {models_str}")
|
|
@@ -267,12 +298,20 @@ def analyze_imgs(img_urls: list[str], user_input: str) -> str:
|
|
|
267
298
|
try:
|
|
268
299
|
if model.startswith("gemini-"):
|
|
269
300
|
print(f"[analyze_imgs] 嘗試使用 Gemini 模型: {model}")
|
|
270
|
-
result = analyze_imgs_with_gemini(img_urls, user_input, model)
|
|
271
|
-
|
|
301
|
+
result, usage = analyze_imgs_with_gemini(img_urls, user_input, model)
|
|
302
|
+
usage_list.append(usage)
|
|
303
|
+
return {
|
|
304
|
+
"result": result,
|
|
305
|
+
"usage_metadata": [u.to_dict() for u in usage_list],
|
|
306
|
+
}
|
|
272
307
|
elif model.startswith("claude-"):
|
|
273
308
|
print(f"[analyze_imgs] 嘗試使用 Claude 模型: {model}")
|
|
274
|
-
result = analyze_imgs_with_claude(img_urls, user_input, model)
|
|
275
|
-
|
|
309
|
+
result, usage = analyze_imgs_with_claude(img_urls, user_input, model)
|
|
310
|
+
usage_list.append(usage)
|
|
311
|
+
return {
|
|
312
|
+
"result": result,
|
|
313
|
+
"usage_metadata": [u.to_dict() for u in usage_list],
|
|
314
|
+
}
|
|
276
315
|
else:
|
|
277
316
|
print(f"[analyze_imgs] 不支持的模型格式: {model}, 跳過")
|
|
278
317
|
errors.append(f"不支持的模型格式: {model}")
|
|
@@ -291,4 +330,7 @@ def analyze_imgs(img_urls: list[str], user_input: str) -> str:
|
|
|
291
330
|
|
|
292
331
|
# If we've tried all models and none succeeded, return all errors
|
|
293
332
|
error_summary = "\n".join(errors)
|
|
294
|
-
return
|
|
333
|
+
return {
|
|
334
|
+
"result": f"錯誤: 所有配置的模型都失敗了。詳細錯誤:\n{error_summary}",
|
|
335
|
+
"usage_metadata": [u.to_dict() for u in usage_list],
|
|
336
|
+
}
|
|
@@ -11,11 +11,13 @@ import asyncio
|
|
|
11
11
|
import base64
|
|
12
12
|
import httpx
|
|
13
13
|
import os
|
|
14
|
-
from typing import List, Dict, Any
|
|
14
|
+
from typing import List, Dict, Any, Tuple
|
|
15
15
|
|
|
16
16
|
from dotenv import load_dotenv
|
|
17
17
|
from google.oauth2 import service_account
|
|
18
18
|
|
|
19
|
+
from botrun_flow_lang.langgraph_agents.agents.util.usage_metadata import UsageMetadata
|
|
20
|
+
|
|
19
21
|
load_dotenv()
|
|
20
22
|
|
|
21
23
|
# 檔案大小閾值(MB)
|
|
@@ -30,16 +32,17 @@ MAX_CONCURRENT_CHUNKS = 5
|
|
|
30
32
|
|
|
31
33
|
def analyze_pdf_with_claude(
|
|
32
34
|
pdf_data: str, user_input: str, model_name: str = "claude-sonnet-4-5-20250929"
|
|
33
|
-
):
|
|
35
|
+
) -> Tuple[str, UsageMetadata]:
|
|
34
36
|
"""
|
|
35
37
|
Analyze a PDF file using Claude API
|
|
36
38
|
|
|
37
39
|
Args:
|
|
38
40
|
pdf_data: Base64-encoded PDF data
|
|
39
41
|
user_input: User's query about the PDF content
|
|
42
|
+
model_name: Claude model name to use
|
|
40
43
|
|
|
41
44
|
Returns:
|
|
42
|
-
str: Claude's analysis
|
|
45
|
+
Tuple[str, UsageMetadata]: Claude's analysis and usage metadata
|
|
43
46
|
"""
|
|
44
47
|
# Initialize Anthropic client
|
|
45
48
|
client = anthropic.Anthropic()
|
|
@@ -66,15 +69,25 @@ def analyze_pdf_with_claude(
|
|
|
66
69
|
],
|
|
67
70
|
)
|
|
68
71
|
|
|
72
|
+
# Extract usage metadata
|
|
73
|
+
usage = UsageMetadata(
|
|
74
|
+
prompt_tokens=message.usage.input_tokens,
|
|
75
|
+
completion_tokens=message.usage.output_tokens,
|
|
76
|
+
total_tokens=message.usage.input_tokens + message.usage.output_tokens,
|
|
77
|
+
cache_creation_input_tokens=getattr(message.usage, 'cache_creation_input_tokens', 0) or 0,
|
|
78
|
+
cache_read_input_tokens=getattr(message.usage, 'cache_read_input_tokens', 0) or 0,
|
|
79
|
+
model=model_name,
|
|
80
|
+
)
|
|
81
|
+
|
|
69
82
|
print(
|
|
70
83
|
f"analyze_pdf_with_claude============> input_token: {message.usage.input_tokens} output_token: {message.usage.output_tokens}",
|
|
71
84
|
)
|
|
72
|
-
return message.content[0].text
|
|
85
|
+
return message.content[0].text, usage
|
|
73
86
|
|
|
74
87
|
|
|
75
88
|
def analyze_pdf_with_gemini(
|
|
76
89
|
pdf_data: str, user_input: str, model_name: str = "gemini-2.5-flash", pdf_url: str = ""
|
|
77
|
-
):
|
|
90
|
+
) -> Tuple[str, UsageMetadata]:
|
|
78
91
|
"""
|
|
79
92
|
Analyze a PDF file using Gemini API
|
|
80
93
|
|
|
@@ -82,9 +95,10 @@ def analyze_pdf_with_gemini(
|
|
|
82
95
|
pdf_data: Base64-encoded PDF data
|
|
83
96
|
user_input: User's query about the PDF content
|
|
84
97
|
model_name: Gemini model name to use
|
|
98
|
+
pdf_url: Original PDF URL for logging
|
|
85
99
|
|
|
86
100
|
Returns:
|
|
87
|
-
str: Gemini's analysis
|
|
101
|
+
Tuple[str, UsageMetadata]: Gemini's analysis and usage metadata
|
|
88
102
|
"""
|
|
89
103
|
# 放到要用的時候才 import,不然loading 會花時間
|
|
90
104
|
from google import genai
|
|
@@ -112,14 +126,25 @@ def analyze_pdf_with_gemini(
|
|
|
112
126
|
),
|
|
113
127
|
],
|
|
114
128
|
)
|
|
115
|
-
|
|
129
|
+
|
|
130
|
+
# Extract usage metadata
|
|
131
|
+
usage = UsageMetadata(model=model_name)
|
|
116
132
|
if hasattr(response, "usage_metadata"):
|
|
133
|
+
usage_meta = response.usage_metadata
|
|
134
|
+
usage = UsageMetadata(
|
|
135
|
+
prompt_tokens=getattr(usage_meta, 'prompt_token_count', 0) or 0,
|
|
136
|
+
completion_tokens=getattr(usage_meta, 'candidates_token_count', 0) or 0,
|
|
137
|
+
total_tokens=getattr(usage_meta, 'total_token_count', 0) or 0,
|
|
138
|
+
cache_creation_input_tokens=0,
|
|
139
|
+
cache_read_input_tokens=getattr(usage_meta, 'cached_content_token_count', 0) or 0,
|
|
140
|
+
model=model_name,
|
|
141
|
+
)
|
|
117
142
|
print(
|
|
118
|
-
f"analyze_pdf_with_gemini============> input_token: {
|
|
143
|
+
f"analyze_pdf_with_gemini============> input_token: {usage_meta.prompt_token_count} output_token: {usage_meta.candidates_token_count}",
|
|
119
144
|
)
|
|
120
145
|
|
|
121
146
|
print(f"{pdf_url} success")
|
|
122
|
-
return response.text
|
|
147
|
+
return response.text, usage
|
|
123
148
|
|
|
124
149
|
|
|
125
150
|
def _analyze_single_chunk(
|
|
@@ -135,7 +160,7 @@ def _analyze_single_chunk(
|
|
|
135
160
|
model_name: 使用的模型名稱
|
|
136
161
|
|
|
137
162
|
Returns:
|
|
138
|
-
Dict: {"page_range": str, "answer": str, "relevant": bool, "error": str|None}
|
|
163
|
+
Dict: {"page_range": str, "answer": str, "relevant": bool, "error": str|None, "usage": UsageMetadata}
|
|
139
164
|
"""
|
|
140
165
|
# 構建切片專用的 prompt
|
|
141
166
|
chunk_prompt = f"""你正在閱讀一份大型 PDF 文件的其中一部分({page_range})。
|
|
@@ -149,15 +174,16 @@ def _analyze_single_chunk(
|
|
|
149
174
|
|
|
150
175
|
try:
|
|
151
176
|
if model_name.startswith("gemini-"):
|
|
152
|
-
answer = analyze_pdf_with_gemini(chunk_data, chunk_prompt, model_name)
|
|
177
|
+
answer, usage = analyze_pdf_with_gemini(chunk_data, chunk_prompt, model_name)
|
|
153
178
|
elif model_name.startswith("claude-"):
|
|
154
|
-
answer = analyze_pdf_with_claude(chunk_data, chunk_prompt, model_name)
|
|
179
|
+
answer, usage = analyze_pdf_with_claude(chunk_data, chunk_prompt, model_name)
|
|
155
180
|
else:
|
|
156
181
|
return {
|
|
157
182
|
"page_range": page_range,
|
|
158
183
|
"answer": "",
|
|
159
184
|
"relevant": False,
|
|
160
185
|
"error": f"Unknown model type: {model_name}",
|
|
186
|
+
"usage": UsageMetadata(),
|
|
161
187
|
}
|
|
162
188
|
|
|
163
189
|
# 判斷是否相關
|
|
@@ -168,6 +194,7 @@ def _analyze_single_chunk(
|
|
|
168
194
|
"answer": answer if is_relevant else "",
|
|
169
195
|
"relevant": is_relevant,
|
|
170
196
|
"error": None,
|
|
197
|
+
"usage": usage,
|
|
171
198
|
}
|
|
172
199
|
|
|
173
200
|
except Exception as e:
|
|
@@ -179,12 +206,13 @@ def _analyze_single_chunk(
|
|
|
179
206
|
"answer": "",
|
|
180
207
|
"relevant": False,
|
|
181
208
|
"error": str(e),
|
|
209
|
+
"usage": UsageMetadata(model=model_name),
|
|
182
210
|
}
|
|
183
211
|
|
|
184
212
|
|
|
185
213
|
async def analyze_pdf_chunks_parallel(
|
|
186
214
|
chunks: List[tuple], user_input: str, model_name: str, max_concurrent: int = 5
|
|
187
|
-
) -> List[Dict[str, Any]]:
|
|
215
|
+
) -> Tuple[List[Dict[str, Any]], List[UsageMetadata]]:
|
|
188
216
|
"""
|
|
189
217
|
平行問答多個 PDF 切片
|
|
190
218
|
|
|
@@ -195,7 +223,7 @@ async def analyze_pdf_chunks_parallel(
|
|
|
195
223
|
max_concurrent: 最大平行數量
|
|
196
224
|
|
|
197
225
|
Returns:
|
|
198
|
-
List[Dict]:
|
|
226
|
+
Tuple[List[Dict], List[UsageMetadata]]: 每個切片的回答結果和每次呼叫的 usage list
|
|
199
227
|
"""
|
|
200
228
|
semaphore = asyncio.Semaphore(max_concurrent)
|
|
201
229
|
|
|
@@ -224,8 +252,9 @@ async def analyze_pdf_chunks_parallel(
|
|
|
224
252
|
# 平行執行
|
|
225
253
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
226
254
|
|
|
227
|
-
#
|
|
255
|
+
# 處理例外並收集 usage list
|
|
228
256
|
processed_results = []
|
|
257
|
+
usage_list = []
|
|
229
258
|
for i, result in enumerate(results):
|
|
230
259
|
if isinstance(result, Exception):
|
|
231
260
|
processed_results.append(
|
|
@@ -234,19 +263,24 @@ async def analyze_pdf_chunks_parallel(
|
|
|
234
263
|
"answer": "",
|
|
235
264
|
"relevant": False,
|
|
236
265
|
"error": str(result),
|
|
266
|
+
"usage": UsageMetadata(model=model_name),
|
|
237
267
|
}
|
|
238
268
|
)
|
|
269
|
+
usage_list.append(UsageMetadata(model=model_name))
|
|
239
270
|
else:
|
|
240
271
|
processed_results.append(result)
|
|
272
|
+
# 收集 usage
|
|
273
|
+
if "usage" in result and isinstance(result["usage"], UsageMetadata):
|
|
274
|
+
usage_list.append(result["usage"])
|
|
241
275
|
|
|
242
|
-
return processed_results
|
|
276
|
+
return processed_results, usage_list
|
|
243
277
|
|
|
244
278
|
|
|
245
279
|
def merge_chunk_results(
|
|
246
280
|
chunk_results: List[Dict[str, Any]],
|
|
247
281
|
user_input: str,
|
|
248
282
|
model_name: str = "gemini-2.5-flash",
|
|
249
|
-
) -> str:
|
|
283
|
+
) -> Tuple[str, UsageMetadata]:
|
|
250
284
|
"""
|
|
251
285
|
使用 LLM 統整多個切片的回答
|
|
252
286
|
|
|
@@ -256,7 +290,7 @@ def merge_chunk_results(
|
|
|
256
290
|
model_name: 統整使用的模型名稱
|
|
257
291
|
|
|
258
292
|
Returns:
|
|
259
|
-
str:
|
|
293
|
+
Tuple[str, UsageMetadata]: 統整後的回答和 usage metadata
|
|
260
294
|
"""
|
|
261
295
|
# 過濾出相關的回答
|
|
262
296
|
relevant_results = [r for r in chunk_results if r.get("relevant", False)]
|
|
@@ -266,12 +300,12 @@ def merge_chunk_results(
|
|
|
266
300
|
error_results = [r for r in chunk_results if r.get("error")]
|
|
267
301
|
if error_results:
|
|
268
302
|
error_msgs = [f"{r['page_range']}: {r['error']}" for r in error_results]
|
|
269
|
-
return f"分析 PDF 時發生錯誤:\n" + "\n".join(error_msgs)
|
|
270
|
-
return "在 PDF 文件中未找到與您問題相關的內容。"
|
|
303
|
+
return f"分析 PDF 時發生錯誤:\n" + "\n".join(error_msgs), UsageMetadata(model=model_name)
|
|
304
|
+
return "在 PDF 文件中未找到與您問題相關的內容。", UsageMetadata(model=model_name)
|
|
271
305
|
|
|
272
|
-
#
|
|
306
|
+
# 只有一個相關結果,直接回傳(不需要額外的 LLM 呼叫)
|
|
273
307
|
if len(relevant_results) == 1:
|
|
274
|
-
return relevant_results[0]["answer"]
|
|
308
|
+
return relevant_results[0]["answer"], UsageMetadata(model=model_name)
|
|
275
309
|
|
|
276
310
|
# 多個相關結果,需要統整
|
|
277
311
|
combined_content = "\n\n".join(
|
|
@@ -310,22 +344,33 @@ def merge_chunk_results(
|
|
|
310
344
|
contents=[merge_prompt],
|
|
311
345
|
)
|
|
312
346
|
|
|
347
|
+
# Extract usage metadata
|
|
348
|
+
usage = UsageMetadata(model=model_name)
|
|
313
349
|
if hasattr(response, "usage_metadata"):
|
|
350
|
+
usage_meta = response.usage_metadata
|
|
351
|
+
usage = UsageMetadata(
|
|
352
|
+
prompt_tokens=getattr(usage_meta, 'prompt_token_count', 0) or 0,
|
|
353
|
+
completion_tokens=getattr(usage_meta, 'candidates_token_count', 0) or 0,
|
|
354
|
+
total_tokens=getattr(usage_meta, 'total_token_count', 0) or 0,
|
|
355
|
+
cache_creation_input_tokens=0,
|
|
356
|
+
cache_read_input_tokens=getattr(usage_meta, 'cached_content_token_count', 0) or 0,
|
|
357
|
+
model=model_name,
|
|
358
|
+
)
|
|
314
359
|
print(
|
|
315
|
-
f"merge_chunk_results============> input_token: {
|
|
360
|
+
f"merge_chunk_results============> input_token: {usage_meta.prompt_token_count} output_token: {usage_meta.candidates_token_count}",
|
|
316
361
|
)
|
|
317
362
|
|
|
318
|
-
return response.text
|
|
363
|
+
return response.text, usage
|
|
319
364
|
|
|
320
365
|
except Exception as e:
|
|
321
366
|
import traceback
|
|
322
367
|
|
|
323
368
|
traceback.print_exc()
|
|
324
369
|
# 統整失敗,直接回傳合併的內容
|
|
325
|
-
return f"統整時發生錯誤,以下是各部分的回答:\n\n{combined_content}"
|
|
370
|
+
return f"統整時發生錯誤,以下是各部分的回答:\n\n{combined_content}", UsageMetadata(model=model_name)
|
|
326
371
|
|
|
327
372
|
|
|
328
|
-
async def analyze_pdf_async(pdf_url: str, user_input: str) -> str:
|
|
373
|
+
async def analyze_pdf_async(pdf_url: str, user_input: str) -> Dict[str, Any]:
|
|
329
374
|
"""
|
|
330
375
|
非同步分析 PDF 檔案(智慧處理策略)
|
|
331
376
|
|
|
@@ -338,8 +383,13 @@ async def analyze_pdf_async(pdf_url: str, user_input: str) -> str:
|
|
|
338
383
|
user_input: 使用者問題
|
|
339
384
|
|
|
340
385
|
Returns:
|
|
341
|
-
str:
|
|
386
|
+
Dict[str, Any]: {
|
|
387
|
+
"result": str, # 分析結果
|
|
388
|
+
"usage_metadata": List[Dict] # 每次 LLM 呼叫的 usage 資訊
|
|
389
|
+
}
|
|
342
390
|
"""
|
|
391
|
+
usage_list: List[UsageMetadata] = []
|
|
392
|
+
|
|
343
393
|
try:
|
|
344
394
|
# 1. 下載 PDF
|
|
345
395
|
print(f"[analyze_pdf_async] 下載 PDF: {pdf_url}")
|
|
@@ -364,9 +414,19 @@ async def analyze_pdf_async(pdf_url: str, user_input: str) -> str:
|
|
|
364
414
|
for model in models:
|
|
365
415
|
try:
|
|
366
416
|
if model.startswith("gemini-"):
|
|
367
|
-
|
|
417
|
+
result, usage = analyze_pdf_with_gemini(pdf_data, user_input, model, pdf_url)
|
|
418
|
+
usage_list.append(usage)
|
|
419
|
+
return {
|
|
420
|
+
"result": result,
|
|
421
|
+
"usage_metadata": [u.to_dict() for u in usage_list],
|
|
422
|
+
}
|
|
368
423
|
elif model.startswith("claude-"):
|
|
369
|
-
|
|
424
|
+
result, usage = analyze_pdf_with_claude(pdf_data, user_input, model)
|
|
425
|
+
usage_list.append(usage)
|
|
426
|
+
return {
|
|
427
|
+
"result": result,
|
|
428
|
+
"usage_metadata": [u.to_dict() for u in usage_list],
|
|
429
|
+
}
|
|
370
430
|
except Exception as e:
|
|
371
431
|
import traceback
|
|
372
432
|
|
|
@@ -374,7 +434,10 @@ async def analyze_pdf_async(pdf_url: str, user_input: str) -> str:
|
|
|
374
434
|
last_error = str(e)
|
|
375
435
|
continue
|
|
376
436
|
|
|
377
|
-
return
|
|
437
|
+
return {
|
|
438
|
+
"result": f"分析 PDF 時所有模型都失敗。最後錯誤: {last_error}",
|
|
439
|
+
"usage_metadata": [u.to_dict() for u in usage_list],
|
|
440
|
+
}
|
|
378
441
|
|
|
379
442
|
# 3. 大檔:壓縮 → 切割 → 平行問答 → 統整
|
|
380
443
|
print(f"[analyze_pdf_async] 大檔模式 (>= {PDF_SIZE_THRESHOLD_MB}MB)")
|
|
@@ -427,9 +490,10 @@ async def analyze_pdf_async(pdf_url: str, user_input: str) -> str:
|
|
|
427
490
|
|
|
428
491
|
# 3.3 平行問答
|
|
429
492
|
print(f"[analyze_pdf_async] 開始平行問答 (最大並行: {MAX_CONCURRENT_CHUNKS})...")
|
|
430
|
-
chunk_results = await analyze_pdf_chunks_parallel(
|
|
493
|
+
chunk_results, chunk_usage_list = await analyze_pdf_chunks_parallel(
|
|
431
494
|
chunks, user_input, primary_model, max_concurrent=MAX_CONCURRENT_CHUNKS
|
|
432
495
|
)
|
|
496
|
+
usage_list.extend(chunk_usage_list)
|
|
433
497
|
|
|
434
498
|
# 統計結果
|
|
435
499
|
relevant_count = sum(1 for r in chunk_results if r.get("relevant", False))
|
|
@@ -441,19 +505,28 @@ async def analyze_pdf_async(pdf_url: str, user_input: str) -> str:
|
|
|
441
505
|
|
|
442
506
|
# 3.4 統整結果
|
|
443
507
|
print("[analyze_pdf_async] 統整結果...")
|
|
444
|
-
result = merge_chunk_results(chunk_results, user_input, primary_model)
|
|
508
|
+
result, merge_usage = merge_chunk_results(chunk_results, user_input, primary_model)
|
|
509
|
+
# 只有當 merge_usage 有實際 token 使用時才加入(避免加入空的 usage)
|
|
510
|
+
if merge_usage.prompt_tokens > 0 or merge_usage.completion_tokens > 0:
|
|
511
|
+
usage_list.append(merge_usage)
|
|
445
512
|
print("[analyze_pdf_async] 完成")
|
|
446
513
|
|
|
447
|
-
return
|
|
514
|
+
return {
|
|
515
|
+
"result": result,
|
|
516
|
+
"usage_metadata": [u.to_dict() for u in usage_list],
|
|
517
|
+
}
|
|
448
518
|
|
|
449
519
|
except Exception as e:
|
|
450
520
|
import traceback
|
|
451
521
|
|
|
452
522
|
traceback.print_exc()
|
|
453
|
-
return
|
|
523
|
+
return {
|
|
524
|
+
"result": f"分析 PDF {pdf_url} 時發生錯誤: {str(e)}",
|
|
525
|
+
"usage_metadata": [u.to_dict() for u in usage_list],
|
|
526
|
+
}
|
|
454
527
|
|
|
455
528
|
|
|
456
|
-
def analyze_pdf(pdf_url: str, user_input: str) -> str:
|
|
529
|
+
def analyze_pdf(pdf_url: str, user_input: str) -> Dict[str, Any]:
|
|
457
530
|
"""
|
|
458
531
|
分析 PDF 檔案(同步包裝函數)
|
|
459
532
|
|
|
@@ -465,7 +538,10 @@ def analyze_pdf(pdf_url: str, user_input: str) -> str:
|
|
|
465
538
|
user_input: 使用者問題
|
|
466
539
|
|
|
467
540
|
Returns:
|
|
468
|
-
str:
|
|
541
|
+
Dict[str, Any]: {
|
|
542
|
+
"result": str, # 分析結果
|
|
543
|
+
"usage_metadata": List[Dict] # 每次 LLM 呼叫的 usage 資訊
|
|
544
|
+
}
|
|
469
545
|
"""
|
|
470
546
|
try:
|
|
471
547
|
# 嘗試取得現有的事件迴圈
|