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.
Files changed (153) hide show
  1. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/CHANGELOG.md +12 -0
  2. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/PKG-INFO +6 -6
  3. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/langgraph_api.py +8 -3
  4. botrun_flow_lang-6.2.21/botrun_flow_lang/api/langgraph_constants.py +11 -0
  5. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/langgraph_react_agent.py +11 -4
  6. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/img_util.py +58 -16
  7. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/pdf_analyzer.py +112 -36
  8. botrun_flow_lang-6.2.21/botrun_flow_lang/langgraph_agents/agents/util/usage_metadata.py +34 -0
  9. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/mcp_server/default_mcp.py +152 -42
  10. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/pyproject.toml +88 -87
  11. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/uv.lock +3377 -3090
  12. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/.env_template +0 -0
  13. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/.gcloudignore +0 -0
  14. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/.gitignore +0 -0
  15. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/.vscode/launch.json +0 -0
  16. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/CLAUDE.md +0 -0
  17. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/Dockerfile.template +0 -0
  18. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/README.md +0 -0
  19. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/__init__.py +0 -0
  20. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/__init__.py +0 -0
  21. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/auth_api.py +0 -0
  22. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/auth_utils.py +0 -0
  23. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/botrun_back_api.py +0 -0
  24. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/flow_api.py +0 -0
  25. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/hatch_api.py +0 -0
  26. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/line_bot_api.py +0 -0
  27. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/model_api.py +0 -0
  28. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/rate_limit_api.py +0 -0
  29. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/routes.py +0 -0
  30. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/search_api.py +0 -0
  31. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/storage_api.py +0 -0
  32. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/subsidy_api.py +0 -0
  33. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/subsidy_api_system_prompt.txt +0 -0
  34. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/user_setting_api.py +0 -0
  35. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/version_api.py +0 -0
  36. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/api/youtube_api.py +0 -0
  37. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/constants.py +0 -0
  38. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/__init__.py +0 -0
  39. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/__init__.py +0 -0
  40. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/agent_runner.py +0 -0
  41. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/agent_tools/__init__.py +0 -0
  42. {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
  43. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/checkpointer/__init__.py +0 -0
  44. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/checkpointer/firestore_checkpointer.py +0 -0
  45. {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
  46. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/gov_researcher/__init__.py +0 -0
  47. {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
  48. {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
  49. {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
  50. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/search_agent_graph.py +0 -0
  51. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/tools/__init__.py +0 -0
  52. {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
  53. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/__init__.py +0 -0
  54. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/gemini_grounding.py +0 -0
  55. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/html_util.py +0 -0
  56. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/local_files.py +0 -0
  57. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/mermaid_util.py +0 -0
  58. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/model_utils.py +0 -0
  59. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/pdf_cache.py +0 -0
  60. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/pdf_processor.py +0 -0
  61. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/perplexity_search.py +0 -0
  62. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/plotly_util.py +0 -0
  63. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/tavily_search.py +0 -0
  64. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/agents/util/youtube_util.py +0 -0
  65. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/cache/__init__.py +0 -0
  66. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/langgraph_agents/cache/langgraph_botrun_cache.py +0 -0
  67. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/llm_agent/__init__.py +0 -0
  68. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/llm_agent/llm_agent.py +0 -0
  69. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/llm_agent/llm_agent_util.py +0 -0
  70. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/log/.gitignore +0 -0
  71. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/main.py +0 -0
  72. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/main_fast.py +0 -0
  73. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/mcp_server/__init__.py +0 -0
  74. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/models/__init__.py +0 -0
  75. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/models/nodes/utils.py +0 -0
  76. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/models/token_usage.py +0 -0
  77. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/requirements.txt +0 -0
  78. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/__init__.py +0 -0
  79. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/base/firestore_base.py +0 -0
  80. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/hatch/__init__.py +0 -0
  81. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/hatch/hatch_factory.py +0 -0
  82. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/hatch/hatch_fs_store.py +0 -0
  83. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/storage/__init__.py +0 -0
  84. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/storage/storage_cs_store.py +0 -0
  85. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/storage/storage_factory.py +0 -0
  86. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/storage/storage_store.py +0 -0
  87. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/user_setting/__init__.py +0 -0
  88. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/services/user_setting/user_setting_factory.py +0 -0
  89. {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
  90. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/static/docs/tools/index.html +0 -0
  91. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/api_functional_tests.py +0 -0
  92. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/api_stress_test.py +0 -0
  93. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/shared_hatch_tests.py +0 -0
  94. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_botrun_app.py +0 -0
  95. {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
  96. {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
  97. {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
  98. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_files/ImportedPhoto.760363950.029251.jpeg +0 -0
  99. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_files/ImportedPhoto.760363950.030446.jpeg +0 -0
  100. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_files/ImportedPhoto.760363950.031127.jpeg +0 -0
  101. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_files/d5712343.jpg +0 -0
  102. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_files/spot_difference_1.png +0 -0
  103. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_files/spot_difference_2.png +0 -0
  104. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_html_util.py +0 -0
  105. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_img_analyzer.py +0 -0
  106. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_img_util.py +0 -0
  107. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_local_files.py +0 -0
  108. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_mermaid_util.py +0 -0
  109. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_pdf_analyzer.py +0 -0
  110. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_plotly_util.py +0 -0
  111. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tests/test_run_workflow_engine.py +0 -0
  112. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tools/generate_docs.py +0 -0
  113. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/tools/templates/tools.html +0 -0
  114. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/utils/__init__.py +0 -0
  115. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/utils/botrun_logger.py +0 -0
  116. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/utils/clients/__init__.py +0 -0
  117. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/utils/clients/rate_limit_client.py +0 -0
  118. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/utils/clients/token_verify_client.py +0 -0
  119. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/utils/google_drive_utils.py +0 -0
  120. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/utils/langchain_utils.py +0 -0
  121. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/botrun_flow_lang/utils/yaml_utils.py +0 -0
  122. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/cloudbuild_template.yaml +0 -0
  123. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/convert_newlines.sh +0 -0
  124. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/deploy.sh +0 -0
  125. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/langgraph.json +0 -0
  126. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/poetry.lock +0 -0
  127. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/poetry.toml +0 -0
  128. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/requirements.txt +0 -0
  129. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/requirements_fast.txt +0 -0
  130. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/deploy_bigline.sh +0 -0
  131. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/deploy_dev.sh +0 -0
  132. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/deploy_evaldev.sh +0 -0
  133. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/deploy_fast.sh +0 -0
  134. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/deploy_modaline.sh +0 -0
  135. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/deploy_prod.sh +0 -0
  136. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/deploy_sebaline.sh +0 -0
  137. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/firestore_checkpointer_delete_thread.py +0 -0
  138. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/generate_docs.sh +0 -0
  139. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/llm_time_test.py +0 -0
  140. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/run_langgraph.py +0 -0
  141. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/run_langgraph_react.py +0 -0
  142. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/run_youtube_summary.py +0 -0
  143. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/scrape_pdf.py +0 -0
  144. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/seba_pypi.sh +0 -0
  145. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/sh/subsidy_test.py +0 -0
  146. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/specs/gov-search/agent-architecture-upgrade.excalidraw +0 -0
  147. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/specs/gov-search/line_dev.excalidraw +0 -0
  148. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/specs/gov-search/subsidy_agent.excalidraw +0 -0
  149. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/specs/gov-search/subsidy_agent_dev.excalidraw +0 -0
  150. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/specs/gov-search/temp_txt.md +0 -0
  151. {botrun_flow_lang-5.12.264 → botrun_flow_lang-6.2.21}/specs/gov-search/temp_txt2.md +0 -0
  152. {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
  153. {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: 5.12.264
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.2.17
30
+ Requires-Dist: langchain-aws>=1.0.0
31
31
  Requires-Dist: langchain-community>=0.3.27
32
- Requires-Dist: langchain-core>=0.3.72
32
+ Requires-Dist: langchain-core>=1.1.2
33
33
  Requires-Dist: langchain-google-community>=2.0.3
34
- Requires-Dist: langchain-google-genai>=2.0.9
35
- Requires-Dist: langchain-google-vertexai<3.0.0,>=2.1.2
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<2,>=1
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
- PERPLEXITY_SEARCH_AGENT = "perplexity_search_agent"
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
- from langchain_google_vertexai import ChatVertexAI
90
- from langchain_google_vertexai.model_garden import ChatAnthropicVertex
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
- model = ChatVertexAI(
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 ChatVertexAI {vertex_model_name} @ {vertex_region} (project: {vertex_project})"
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 of the image content(s) based on the query
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 of the image content(s) based on the query
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
- print(
220
- f"analyze_imgs_with_gemini============> input_token: {response.usage_metadata.prompt_token_count} output_token: {response.usage_metadata.candidates_token_count}"
221
- )
222
- return response.text
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: AI analysis of the image content(s) based on the query
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
- return result
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
- return result
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 f"錯誤: 所有配置的模型都失敗了。詳細錯誤:\n{error_summary}"
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 of the PDF content based on the query
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 of the PDF content based on the query
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
- # Log token usage if available
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: {response.usage_metadata.prompt_token_count} output_token: {response.usage_metadata.candidates_token_count}",
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: {response.usage_metadata.prompt_token_count} output_token: {response.usage_metadata.candidates_token_count}",
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
- return analyze_pdf_with_gemini(pdf_data, user_input, model, pdf_url)
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
- return analyze_pdf_with_claude(pdf_data, user_input, model)
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 f"分析 PDF 時所有模型都失敗。最後錯誤: {last_error}"
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 result
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 f"分析 PDF {pdf_url} 時發生錯誤: {str(e)}"
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
  # 嘗試取得現有的事件迴圈