aiecs 1.0.1__py3-none-any.whl → 1.7.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of aiecs might be problematic. Click here for more details.

Files changed (340) hide show
  1. aiecs/__init__.py +13 -16
  2. aiecs/__main__.py +7 -7
  3. aiecs/aiecs_client.py +269 -75
  4. aiecs/application/executors/operation_executor.py +79 -54
  5. aiecs/application/knowledge_graph/__init__.py +7 -0
  6. aiecs/application/knowledge_graph/builder/__init__.py +37 -0
  7. aiecs/application/knowledge_graph/builder/data_quality.py +302 -0
  8. aiecs/application/knowledge_graph/builder/data_reshaping.py +293 -0
  9. aiecs/application/knowledge_graph/builder/document_builder.py +369 -0
  10. aiecs/application/knowledge_graph/builder/graph_builder.py +490 -0
  11. aiecs/application/knowledge_graph/builder/import_optimizer.py +396 -0
  12. aiecs/application/knowledge_graph/builder/schema_inference.py +462 -0
  13. aiecs/application/knowledge_graph/builder/schema_mapping.py +563 -0
  14. aiecs/application/knowledge_graph/builder/structured_pipeline.py +1384 -0
  15. aiecs/application/knowledge_graph/builder/text_chunker.py +317 -0
  16. aiecs/application/knowledge_graph/extractors/__init__.py +27 -0
  17. aiecs/application/knowledge_graph/extractors/base.py +98 -0
  18. aiecs/application/knowledge_graph/extractors/llm_entity_extractor.py +422 -0
  19. aiecs/application/knowledge_graph/extractors/llm_relation_extractor.py +347 -0
  20. aiecs/application/knowledge_graph/extractors/ner_entity_extractor.py +241 -0
  21. aiecs/application/knowledge_graph/fusion/__init__.py +78 -0
  22. aiecs/application/knowledge_graph/fusion/ab_testing.py +395 -0
  23. aiecs/application/knowledge_graph/fusion/abbreviation_expander.py +327 -0
  24. aiecs/application/knowledge_graph/fusion/alias_index.py +597 -0
  25. aiecs/application/knowledge_graph/fusion/alias_matcher.py +384 -0
  26. aiecs/application/knowledge_graph/fusion/cache_coordinator.py +343 -0
  27. aiecs/application/knowledge_graph/fusion/entity_deduplicator.py +433 -0
  28. aiecs/application/knowledge_graph/fusion/entity_linker.py +511 -0
  29. aiecs/application/knowledge_graph/fusion/evaluation_dataset.py +240 -0
  30. aiecs/application/knowledge_graph/fusion/knowledge_fusion.py +632 -0
  31. aiecs/application/knowledge_graph/fusion/matching_config.py +489 -0
  32. aiecs/application/knowledge_graph/fusion/name_normalizer.py +352 -0
  33. aiecs/application/knowledge_graph/fusion/relation_deduplicator.py +183 -0
  34. aiecs/application/knowledge_graph/fusion/semantic_name_matcher.py +464 -0
  35. aiecs/application/knowledge_graph/fusion/similarity_pipeline.py +534 -0
  36. aiecs/application/knowledge_graph/pattern_matching/__init__.py +21 -0
  37. aiecs/application/knowledge_graph/pattern_matching/pattern_matcher.py +342 -0
  38. aiecs/application/knowledge_graph/pattern_matching/query_executor.py +366 -0
  39. aiecs/application/knowledge_graph/profiling/__init__.py +12 -0
  40. aiecs/application/knowledge_graph/profiling/query_plan_visualizer.py +195 -0
  41. aiecs/application/knowledge_graph/profiling/query_profiler.py +223 -0
  42. aiecs/application/knowledge_graph/reasoning/__init__.py +27 -0
  43. aiecs/application/knowledge_graph/reasoning/evidence_synthesis.py +341 -0
  44. aiecs/application/knowledge_graph/reasoning/inference_engine.py +500 -0
  45. aiecs/application/knowledge_graph/reasoning/logic_form_parser.py +163 -0
  46. aiecs/application/knowledge_graph/reasoning/logic_parser/__init__.py +79 -0
  47. aiecs/application/knowledge_graph/reasoning/logic_parser/ast_builder.py +513 -0
  48. aiecs/application/knowledge_graph/reasoning/logic_parser/ast_nodes.py +913 -0
  49. aiecs/application/knowledge_graph/reasoning/logic_parser/ast_validator.py +866 -0
  50. aiecs/application/knowledge_graph/reasoning/logic_parser/error_handler.py +475 -0
  51. aiecs/application/knowledge_graph/reasoning/logic_parser/parser.py +396 -0
  52. aiecs/application/knowledge_graph/reasoning/logic_parser/query_context.py +208 -0
  53. aiecs/application/knowledge_graph/reasoning/logic_query_integration.py +170 -0
  54. aiecs/application/knowledge_graph/reasoning/query_planner.py +855 -0
  55. aiecs/application/knowledge_graph/reasoning/reasoning_engine.py +518 -0
  56. aiecs/application/knowledge_graph/retrieval/__init__.py +27 -0
  57. aiecs/application/knowledge_graph/retrieval/query_intent_classifier.py +211 -0
  58. aiecs/application/knowledge_graph/retrieval/retrieval_strategies.py +592 -0
  59. aiecs/application/knowledge_graph/retrieval/strategy_types.py +23 -0
  60. aiecs/application/knowledge_graph/search/__init__.py +59 -0
  61. aiecs/application/knowledge_graph/search/hybrid_search.py +457 -0
  62. aiecs/application/knowledge_graph/search/reranker.py +293 -0
  63. aiecs/application/knowledge_graph/search/reranker_strategies.py +535 -0
  64. aiecs/application/knowledge_graph/search/text_similarity.py +392 -0
  65. aiecs/application/knowledge_graph/traversal/__init__.py +15 -0
  66. aiecs/application/knowledge_graph/traversal/enhanced_traversal.py +305 -0
  67. aiecs/application/knowledge_graph/traversal/path_scorer.py +271 -0
  68. aiecs/application/knowledge_graph/validators/__init__.py +13 -0
  69. aiecs/application/knowledge_graph/validators/relation_validator.py +239 -0
  70. aiecs/application/knowledge_graph/visualization/__init__.py +11 -0
  71. aiecs/application/knowledge_graph/visualization/graph_visualizer.py +313 -0
  72. aiecs/common/__init__.py +9 -0
  73. aiecs/common/knowledge_graph/__init__.py +17 -0
  74. aiecs/common/knowledge_graph/runnable.py +471 -0
  75. aiecs/config/__init__.py +20 -5
  76. aiecs/config/config.py +762 -31
  77. aiecs/config/graph_config.py +131 -0
  78. aiecs/config/tool_config.py +399 -0
  79. aiecs/core/__init__.py +29 -13
  80. aiecs/core/interface/__init__.py +2 -2
  81. aiecs/core/interface/execution_interface.py +22 -22
  82. aiecs/core/interface/storage_interface.py +37 -88
  83. aiecs/core/registry/__init__.py +31 -0
  84. aiecs/core/registry/service_registry.py +92 -0
  85. aiecs/domain/__init__.py +270 -1
  86. aiecs/domain/agent/__init__.py +191 -0
  87. aiecs/domain/agent/base_agent.py +3870 -0
  88. aiecs/domain/agent/exceptions.py +99 -0
  89. aiecs/domain/agent/graph_aware_mixin.py +569 -0
  90. aiecs/domain/agent/hybrid_agent.py +1435 -0
  91. aiecs/domain/agent/integration/__init__.py +29 -0
  92. aiecs/domain/agent/integration/context_compressor.py +216 -0
  93. aiecs/domain/agent/integration/context_engine_adapter.py +587 -0
  94. aiecs/domain/agent/integration/protocols.py +281 -0
  95. aiecs/domain/agent/integration/retry_policy.py +218 -0
  96. aiecs/domain/agent/integration/role_config.py +213 -0
  97. aiecs/domain/agent/knowledge_aware_agent.py +1892 -0
  98. aiecs/domain/agent/lifecycle.py +291 -0
  99. aiecs/domain/agent/llm_agent.py +692 -0
  100. aiecs/domain/agent/memory/__init__.py +12 -0
  101. aiecs/domain/agent/memory/conversation.py +1124 -0
  102. aiecs/domain/agent/migration/__init__.py +14 -0
  103. aiecs/domain/agent/migration/conversion.py +163 -0
  104. aiecs/domain/agent/migration/legacy_wrapper.py +86 -0
  105. aiecs/domain/agent/models.py +884 -0
  106. aiecs/domain/agent/observability.py +479 -0
  107. aiecs/domain/agent/persistence.py +449 -0
  108. aiecs/domain/agent/prompts/__init__.py +29 -0
  109. aiecs/domain/agent/prompts/builder.py +159 -0
  110. aiecs/domain/agent/prompts/formatters.py +187 -0
  111. aiecs/domain/agent/prompts/template.py +255 -0
  112. aiecs/domain/agent/registry.py +253 -0
  113. aiecs/domain/agent/tool_agent.py +444 -0
  114. aiecs/domain/agent/tools/__init__.py +15 -0
  115. aiecs/domain/agent/tools/schema_generator.py +364 -0
  116. aiecs/domain/community/__init__.py +155 -0
  117. aiecs/domain/community/agent_adapter.py +469 -0
  118. aiecs/domain/community/analytics.py +432 -0
  119. aiecs/domain/community/collaborative_workflow.py +648 -0
  120. aiecs/domain/community/communication_hub.py +634 -0
  121. aiecs/domain/community/community_builder.py +320 -0
  122. aiecs/domain/community/community_integration.py +796 -0
  123. aiecs/domain/community/community_manager.py +803 -0
  124. aiecs/domain/community/decision_engine.py +849 -0
  125. aiecs/domain/community/exceptions.py +231 -0
  126. aiecs/domain/community/models/__init__.py +33 -0
  127. aiecs/domain/community/models/community_models.py +234 -0
  128. aiecs/domain/community/resource_manager.py +461 -0
  129. aiecs/domain/community/shared_context_manager.py +589 -0
  130. aiecs/domain/context/__init__.py +40 -10
  131. aiecs/domain/context/context_engine.py +1910 -0
  132. aiecs/domain/context/conversation_models.py +87 -53
  133. aiecs/domain/context/graph_memory.py +582 -0
  134. aiecs/domain/execution/model.py +12 -4
  135. aiecs/domain/knowledge_graph/__init__.py +19 -0
  136. aiecs/domain/knowledge_graph/models/__init__.py +52 -0
  137. aiecs/domain/knowledge_graph/models/entity.py +148 -0
  138. aiecs/domain/knowledge_graph/models/evidence.py +178 -0
  139. aiecs/domain/knowledge_graph/models/inference_rule.py +184 -0
  140. aiecs/domain/knowledge_graph/models/path.py +171 -0
  141. aiecs/domain/knowledge_graph/models/path_pattern.py +171 -0
  142. aiecs/domain/knowledge_graph/models/query.py +261 -0
  143. aiecs/domain/knowledge_graph/models/query_plan.py +181 -0
  144. aiecs/domain/knowledge_graph/models/relation.py +202 -0
  145. aiecs/domain/knowledge_graph/schema/__init__.py +23 -0
  146. aiecs/domain/knowledge_graph/schema/entity_type.py +131 -0
  147. aiecs/domain/knowledge_graph/schema/graph_schema.py +253 -0
  148. aiecs/domain/knowledge_graph/schema/property_schema.py +143 -0
  149. aiecs/domain/knowledge_graph/schema/relation_type.py +163 -0
  150. aiecs/domain/knowledge_graph/schema/schema_manager.py +691 -0
  151. aiecs/domain/knowledge_graph/schema/type_enums.py +209 -0
  152. aiecs/domain/task/dsl_processor.py +172 -56
  153. aiecs/domain/task/model.py +20 -8
  154. aiecs/domain/task/task_context.py +27 -24
  155. aiecs/infrastructure/__init__.py +0 -2
  156. aiecs/infrastructure/graph_storage/__init__.py +11 -0
  157. aiecs/infrastructure/graph_storage/base.py +837 -0
  158. aiecs/infrastructure/graph_storage/batch_operations.py +458 -0
  159. aiecs/infrastructure/graph_storage/cache.py +424 -0
  160. aiecs/infrastructure/graph_storage/distributed.py +223 -0
  161. aiecs/infrastructure/graph_storage/error_handling.py +380 -0
  162. aiecs/infrastructure/graph_storage/graceful_degradation.py +294 -0
  163. aiecs/infrastructure/graph_storage/health_checks.py +378 -0
  164. aiecs/infrastructure/graph_storage/in_memory.py +1197 -0
  165. aiecs/infrastructure/graph_storage/index_optimization.py +446 -0
  166. aiecs/infrastructure/graph_storage/lazy_loading.py +431 -0
  167. aiecs/infrastructure/graph_storage/metrics.py +344 -0
  168. aiecs/infrastructure/graph_storage/migration.py +400 -0
  169. aiecs/infrastructure/graph_storage/pagination.py +483 -0
  170. aiecs/infrastructure/graph_storage/performance_monitoring.py +456 -0
  171. aiecs/infrastructure/graph_storage/postgres.py +1563 -0
  172. aiecs/infrastructure/graph_storage/property_storage.py +353 -0
  173. aiecs/infrastructure/graph_storage/protocols.py +76 -0
  174. aiecs/infrastructure/graph_storage/query_optimizer.py +642 -0
  175. aiecs/infrastructure/graph_storage/schema_cache.py +290 -0
  176. aiecs/infrastructure/graph_storage/sqlite.py +1373 -0
  177. aiecs/infrastructure/graph_storage/streaming.py +487 -0
  178. aiecs/infrastructure/graph_storage/tenant.py +412 -0
  179. aiecs/infrastructure/messaging/celery_task_manager.py +92 -54
  180. aiecs/infrastructure/messaging/websocket_manager.py +51 -35
  181. aiecs/infrastructure/monitoring/__init__.py +22 -0
  182. aiecs/infrastructure/monitoring/executor_metrics.py +45 -11
  183. aiecs/infrastructure/monitoring/global_metrics_manager.py +212 -0
  184. aiecs/infrastructure/monitoring/structured_logger.py +3 -7
  185. aiecs/infrastructure/monitoring/tracing_manager.py +63 -35
  186. aiecs/infrastructure/persistence/__init__.py +14 -1
  187. aiecs/infrastructure/persistence/context_engine_client.py +184 -0
  188. aiecs/infrastructure/persistence/database_manager.py +67 -43
  189. aiecs/infrastructure/persistence/file_storage.py +180 -103
  190. aiecs/infrastructure/persistence/redis_client.py +74 -21
  191. aiecs/llm/__init__.py +73 -25
  192. aiecs/llm/callbacks/__init__.py +11 -0
  193. aiecs/llm/{custom_callbacks.py → callbacks/custom_callbacks.py} +26 -19
  194. aiecs/llm/client_factory.py +224 -36
  195. aiecs/llm/client_resolver.py +155 -0
  196. aiecs/llm/clients/__init__.py +38 -0
  197. aiecs/llm/clients/base_client.py +324 -0
  198. aiecs/llm/clients/google_function_calling_mixin.py +457 -0
  199. aiecs/llm/clients/googleai_client.py +241 -0
  200. aiecs/llm/clients/openai_client.py +158 -0
  201. aiecs/llm/clients/openai_compatible_mixin.py +367 -0
  202. aiecs/llm/clients/vertex_client.py +897 -0
  203. aiecs/llm/clients/xai_client.py +201 -0
  204. aiecs/llm/config/__init__.py +51 -0
  205. aiecs/llm/config/config_loader.py +272 -0
  206. aiecs/llm/config/config_validator.py +206 -0
  207. aiecs/llm/config/model_config.py +143 -0
  208. aiecs/llm/protocols.py +149 -0
  209. aiecs/llm/utils/__init__.py +10 -0
  210. aiecs/llm/utils/validate_config.py +89 -0
  211. aiecs/main.py +140 -121
  212. aiecs/scripts/aid/VERSION_MANAGEMENT.md +138 -0
  213. aiecs/scripts/aid/__init__.py +19 -0
  214. aiecs/scripts/aid/module_checker.py +499 -0
  215. aiecs/scripts/aid/version_manager.py +235 -0
  216. aiecs/scripts/{DEPENDENCY_SYSTEM_SUMMARY.md → dependance_check/DEPENDENCY_SYSTEM_SUMMARY.md} +1 -0
  217. aiecs/scripts/{README_DEPENDENCY_CHECKER.md → dependance_check/README_DEPENDENCY_CHECKER.md} +1 -0
  218. aiecs/scripts/dependance_check/__init__.py +15 -0
  219. aiecs/scripts/dependance_check/dependency_checker.py +1835 -0
  220. aiecs/scripts/{dependency_fixer.py → dependance_check/dependency_fixer.py} +192 -90
  221. aiecs/scripts/{download_nlp_data.py → dependance_check/download_nlp_data.py} +203 -71
  222. aiecs/scripts/dependance_patch/__init__.py +7 -0
  223. aiecs/scripts/dependance_patch/fix_weasel/__init__.py +11 -0
  224. aiecs/scripts/{fix_weasel_validator.py → dependance_patch/fix_weasel/fix_weasel_validator.py} +21 -14
  225. aiecs/scripts/{patch_weasel_library.sh → dependance_patch/fix_weasel/patch_weasel_library.sh} +1 -1
  226. aiecs/scripts/knowledge_graph/__init__.py +3 -0
  227. aiecs/scripts/knowledge_graph/run_threshold_experiments.py +212 -0
  228. aiecs/scripts/migrations/multi_tenancy/README.md +142 -0
  229. aiecs/scripts/tools_develop/README.md +671 -0
  230. aiecs/scripts/tools_develop/README_CONFIG_CHECKER.md +273 -0
  231. aiecs/scripts/tools_develop/TOOLS_CONFIG_GUIDE.md +1287 -0
  232. aiecs/scripts/tools_develop/TOOL_AUTO_DISCOVERY.md +234 -0
  233. aiecs/scripts/tools_develop/__init__.py +21 -0
  234. aiecs/scripts/tools_develop/check_all_tools_config.py +548 -0
  235. aiecs/scripts/tools_develop/check_type_annotations.py +257 -0
  236. aiecs/scripts/tools_develop/pre-commit-schema-coverage.sh +66 -0
  237. aiecs/scripts/tools_develop/schema_coverage.py +511 -0
  238. aiecs/scripts/tools_develop/validate_tool_schemas.py +475 -0
  239. aiecs/scripts/tools_develop/verify_executor_config_fix.py +98 -0
  240. aiecs/scripts/tools_develop/verify_tools.py +352 -0
  241. aiecs/tasks/__init__.py +0 -1
  242. aiecs/tasks/worker.py +115 -47
  243. aiecs/tools/__init__.py +194 -72
  244. aiecs/tools/apisource/__init__.py +99 -0
  245. aiecs/tools/apisource/intelligence/__init__.py +19 -0
  246. aiecs/tools/apisource/intelligence/data_fusion.py +632 -0
  247. aiecs/tools/apisource/intelligence/query_analyzer.py +417 -0
  248. aiecs/tools/apisource/intelligence/search_enhancer.py +385 -0
  249. aiecs/tools/apisource/monitoring/__init__.py +9 -0
  250. aiecs/tools/apisource/monitoring/metrics.py +330 -0
  251. aiecs/tools/apisource/providers/__init__.py +112 -0
  252. aiecs/tools/apisource/providers/base.py +671 -0
  253. aiecs/tools/apisource/providers/census.py +397 -0
  254. aiecs/tools/apisource/providers/fred.py +535 -0
  255. aiecs/tools/apisource/providers/newsapi.py +409 -0
  256. aiecs/tools/apisource/providers/worldbank.py +352 -0
  257. aiecs/tools/apisource/reliability/__init__.py +12 -0
  258. aiecs/tools/apisource/reliability/error_handler.py +363 -0
  259. aiecs/tools/apisource/reliability/fallback_strategy.py +376 -0
  260. aiecs/tools/apisource/tool.py +832 -0
  261. aiecs/tools/apisource/utils/__init__.py +9 -0
  262. aiecs/tools/apisource/utils/validators.py +334 -0
  263. aiecs/tools/base_tool.py +415 -21
  264. aiecs/tools/docs/__init__.py +121 -0
  265. aiecs/tools/docs/ai_document_orchestrator.py +607 -0
  266. aiecs/tools/docs/ai_document_writer_orchestrator.py +2350 -0
  267. aiecs/tools/docs/content_insertion_tool.py +1320 -0
  268. aiecs/tools/docs/document_creator_tool.py +1323 -0
  269. aiecs/tools/docs/document_layout_tool.py +1160 -0
  270. aiecs/tools/docs/document_parser_tool.py +1011 -0
  271. aiecs/tools/docs/document_writer_tool.py +1829 -0
  272. aiecs/tools/knowledge_graph/__init__.py +17 -0
  273. aiecs/tools/knowledge_graph/graph_reasoning_tool.py +807 -0
  274. aiecs/tools/knowledge_graph/graph_search_tool.py +944 -0
  275. aiecs/tools/knowledge_graph/kg_builder_tool.py +524 -0
  276. aiecs/tools/langchain_adapter.py +300 -138
  277. aiecs/tools/schema_generator.py +455 -0
  278. aiecs/tools/search_tool/__init__.py +100 -0
  279. aiecs/tools/search_tool/analyzers.py +581 -0
  280. aiecs/tools/search_tool/cache.py +264 -0
  281. aiecs/tools/search_tool/constants.py +128 -0
  282. aiecs/tools/search_tool/context.py +224 -0
  283. aiecs/tools/search_tool/core.py +778 -0
  284. aiecs/tools/search_tool/deduplicator.py +119 -0
  285. aiecs/tools/search_tool/error_handler.py +242 -0
  286. aiecs/tools/search_tool/metrics.py +343 -0
  287. aiecs/tools/search_tool/rate_limiter.py +172 -0
  288. aiecs/tools/search_tool/schemas.py +275 -0
  289. aiecs/tools/statistics/__init__.py +80 -0
  290. aiecs/tools/statistics/ai_data_analysis_orchestrator.py +646 -0
  291. aiecs/tools/statistics/ai_insight_generator_tool.py +508 -0
  292. aiecs/tools/statistics/ai_report_orchestrator_tool.py +684 -0
  293. aiecs/tools/statistics/data_loader_tool.py +555 -0
  294. aiecs/tools/statistics/data_profiler_tool.py +638 -0
  295. aiecs/tools/statistics/data_transformer_tool.py +580 -0
  296. aiecs/tools/statistics/data_visualizer_tool.py +498 -0
  297. aiecs/tools/statistics/model_trainer_tool.py +507 -0
  298. aiecs/tools/statistics/statistical_analyzer_tool.py +472 -0
  299. aiecs/tools/task_tools/__init__.py +49 -36
  300. aiecs/tools/task_tools/chart_tool.py +200 -184
  301. aiecs/tools/task_tools/classfire_tool.py +268 -267
  302. aiecs/tools/task_tools/image_tool.py +175 -131
  303. aiecs/tools/task_tools/office_tool.py +226 -146
  304. aiecs/tools/task_tools/pandas_tool.py +477 -121
  305. aiecs/tools/task_tools/report_tool.py +390 -142
  306. aiecs/tools/task_tools/research_tool.py +149 -79
  307. aiecs/tools/task_tools/scraper_tool.py +339 -145
  308. aiecs/tools/task_tools/stats_tool.py +448 -209
  309. aiecs/tools/temp_file_manager.py +26 -24
  310. aiecs/tools/tool_executor/__init__.py +18 -16
  311. aiecs/tools/tool_executor/tool_executor.py +364 -52
  312. aiecs/utils/LLM_output_structor.py +74 -48
  313. aiecs/utils/__init__.py +14 -3
  314. aiecs/utils/base_callback.py +0 -3
  315. aiecs/utils/cache_provider.py +696 -0
  316. aiecs/utils/execution_utils.py +50 -31
  317. aiecs/utils/prompt_loader.py +1 -0
  318. aiecs/utils/token_usage_repository.py +37 -11
  319. aiecs/ws/socket_server.py +14 -4
  320. {aiecs-1.0.1.dist-info → aiecs-1.7.6.dist-info}/METADATA +52 -15
  321. aiecs-1.7.6.dist-info/RECORD +337 -0
  322. aiecs-1.7.6.dist-info/entry_points.txt +13 -0
  323. aiecs/config/registry.py +0 -19
  324. aiecs/domain/context/content_engine.py +0 -982
  325. aiecs/llm/base_client.py +0 -99
  326. aiecs/llm/openai_client.py +0 -125
  327. aiecs/llm/vertex_client.py +0 -186
  328. aiecs/llm/xai_client.py +0 -184
  329. aiecs/scripts/dependency_checker.py +0 -857
  330. aiecs/scripts/quick_dependency_check.py +0 -269
  331. aiecs/tools/task_tools/search_api.py +0 -7
  332. aiecs-1.0.1.dist-info/RECORD +0 -90
  333. aiecs-1.0.1.dist-info/entry_points.txt +0 -7
  334. /aiecs/scripts/{setup_nlp_data.sh → dependance_check/setup_nlp_data.sh} +0 -0
  335. /aiecs/scripts/{README_WEASEL_PATCH.md → dependance_patch/fix_weasel/README_WEASEL_PATCH.md} +0 -0
  336. /aiecs/scripts/{fix_weasel_validator.sh → dependance_patch/fix_weasel/fix_weasel_validator.sh} +0 -0
  337. /aiecs/scripts/{run_weasel_patch.sh → dependance_patch/fix_weasel/run_weasel_patch.sh} +0 -0
  338. {aiecs-1.0.1.dist-info → aiecs-1.7.6.dist-info}/WHEEL +0 -0
  339. {aiecs-1.0.1.dist-info → aiecs-1.7.6.dist-info}/licenses/LICENSE +0 -0
  340. {aiecs-1.0.1.dist-info → aiecs-1.7.6.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,897 @@
1
+ import asyncio
2
+ import logging
3
+ import os
4
+ import warnings
5
+ from typing import Dict, Any, Optional, List, AsyncGenerator
6
+ import vertexai
7
+ from vertexai.generative_models import (
8
+ GenerativeModel,
9
+ HarmCategory,
10
+ HarmBlockThreshold,
11
+ GenerationConfig,
12
+ SafetySetting,
13
+ Content,
14
+ Part,
15
+ )
16
+
17
+ from aiecs.llm.clients.base_client import (
18
+ BaseLLMClient,
19
+ LLMMessage,
20
+ LLMResponse,
21
+ ProviderNotAvailableError,
22
+ RateLimitError,
23
+ SafetyBlockError,
24
+ )
25
+ from aiecs.llm.clients.google_function_calling_mixin import GoogleFunctionCallingMixin
26
+ from aiecs.config.config import get_settings
27
+
28
+ # Suppress Vertex AI SDK deprecation warnings (deprecated June 2025, removal June 2026)
29
+ # TODO: Migrate to Google Gen AI SDK when official migration guide is available
30
+ warnings.filterwarnings(
31
+ "ignore",
32
+ category=UserWarning,
33
+ module="vertexai.generative_models._generative_models",
34
+ )
35
+
36
+ logger = logging.getLogger(__name__)
37
+
38
+
39
+ def _extract_safety_ratings(safety_ratings: Any) -> List[Dict[str, Any]]:
40
+ """
41
+ Extract safety ratings information from Vertex AI response.
42
+
43
+ Args:
44
+ safety_ratings: Safety ratings object from Vertex AI response
45
+
46
+ Returns:
47
+ List of dictionaries containing safety rating details
48
+ """
49
+ ratings_list = []
50
+ if not safety_ratings:
51
+ return ratings_list
52
+
53
+ # Handle both list and single object
54
+ ratings_iter = safety_ratings if isinstance(safety_ratings, list) else [safety_ratings]
55
+
56
+ for rating in ratings_iter:
57
+ rating_dict = {}
58
+
59
+ # Extract category
60
+ if hasattr(rating, "category"):
61
+ rating_dict["category"] = str(rating.category)
62
+ elif hasattr(rating, "get"):
63
+ rating_dict["category"] = rating.get("category", "UNKNOWN")
64
+
65
+ # Extract blocked status
66
+ if hasattr(rating, "blocked"):
67
+ rating_dict["blocked"] = bool(rating.blocked)
68
+ elif hasattr(rating, "get"):
69
+ rating_dict["blocked"] = rating.get("blocked", False)
70
+
71
+ # Extract severity (for HarmBlockMethod.SEVERITY)
72
+ if hasattr(rating, "severity"):
73
+ rating_dict["severity"] = str(rating.severity)
74
+ elif hasattr(rating, "get"):
75
+ rating_dict["severity"] = rating.get("severity")
76
+
77
+ if hasattr(rating, "severity_score"):
78
+ rating_dict["severity_score"] = float(rating.severity_score)
79
+ elif hasattr(rating, "get"):
80
+ rating_dict["severity_score"] = rating.get("severity_score")
81
+
82
+ # Extract probability (for HarmBlockMethod.PROBABILITY)
83
+ if hasattr(rating, "probability"):
84
+ rating_dict["probability"] = str(rating.probability)
85
+ elif hasattr(rating, "get"):
86
+ rating_dict["probability"] = rating.get("probability")
87
+
88
+ if hasattr(rating, "probability_score"):
89
+ rating_dict["probability_score"] = float(rating.probability_score)
90
+ elif hasattr(rating, "get"):
91
+ rating_dict["probability_score"] = rating.get("probability_score")
92
+
93
+ ratings_list.append(rating_dict)
94
+
95
+ return ratings_list
96
+
97
+
98
+ def _build_safety_block_error(
99
+ response: Any,
100
+ block_type: str,
101
+ default_message: str,
102
+ ) -> SafetyBlockError:
103
+ """
104
+ Build a detailed SafetyBlockError from Vertex AI response.
105
+
106
+ Args:
107
+ response: Vertex AI response object
108
+ block_type: "prompt" or "response"
109
+ default_message: Default error message
110
+
111
+ Returns:
112
+ SafetyBlockError with detailed information
113
+ """
114
+ block_reason = None
115
+ safety_ratings = []
116
+
117
+ if block_type == "prompt":
118
+ # Check prompt_feedback for prompt blocks
119
+ if hasattr(response, "prompt_feedback"):
120
+ pf = response.prompt_feedback
121
+ if hasattr(pf, "block_reason"):
122
+ block_reason = str(pf.block_reason)
123
+ elif hasattr(pf, "get"):
124
+ block_reason = pf.get("block_reason")
125
+
126
+ if hasattr(pf, "safety_ratings"):
127
+ safety_ratings = _extract_safety_ratings(pf.safety_ratings)
128
+ elif hasattr(pf, "get"):
129
+ safety_ratings = _extract_safety_ratings(pf.get("safety_ratings", []))
130
+
131
+ elif block_type == "response":
132
+ # Check candidates for response blocks
133
+ if hasattr(response, "candidates") and response.candidates:
134
+ candidate = response.candidates[0]
135
+ if hasattr(candidate, "safety_ratings"):
136
+ safety_ratings = _extract_safety_ratings(candidate.safety_ratings)
137
+ elif hasattr(candidate, "get"):
138
+ safety_ratings = _extract_safety_ratings(candidate.get("safety_ratings", []))
139
+
140
+ # Check finish_reason
141
+ if hasattr(candidate, "finish_reason"):
142
+ finish_reason = str(candidate.finish_reason)
143
+ if finish_reason in ["SAFETY", "RECITATION"]:
144
+ block_reason = finish_reason
145
+
146
+ # Build detailed error message
147
+ error_parts = [default_message]
148
+ if block_reason:
149
+ error_parts.append(f"Block reason: {block_reason}")
150
+
151
+ blocked_categories = [
152
+ r.get("category", "UNKNOWN")
153
+ for r in safety_ratings
154
+ if r.get("blocked", False)
155
+ ]
156
+ if blocked_categories:
157
+ error_parts.append(f"Blocked categories: {', '.join(blocked_categories)}")
158
+
159
+ # Add severity/probability information
160
+ for rating in safety_ratings:
161
+ if rating.get("blocked"):
162
+ if "severity" in rating:
163
+ error_parts.append(
164
+ f"{rating.get('category', 'UNKNOWN')}: severity={rating.get('severity')}, "
165
+ f"score={rating.get('severity_score', 'N/A')}"
166
+ )
167
+ elif "probability" in rating:
168
+ error_parts.append(
169
+ f"{rating.get('category', 'UNKNOWN')}: probability={rating.get('probability')}, "
170
+ f"score={rating.get('probability_score', 'N/A')}"
171
+ )
172
+
173
+ error_message = " | ".join(error_parts)
174
+
175
+ return SafetyBlockError(
176
+ message=error_message,
177
+ block_reason=block_reason,
178
+ block_type=block_type,
179
+ safety_ratings=safety_ratings,
180
+ )
181
+
182
+
183
+ class VertexAIClient(BaseLLMClient, GoogleFunctionCallingMixin):
184
+ """Vertex AI provider client"""
185
+
186
+ def __init__(self):
187
+ super().__init__("Vertex")
188
+ self.settings = get_settings()
189
+ self._initialized = False
190
+ # Track part count statistics for monitoring
191
+ self._part_count_stats = {
192
+ "total_responses": 0,
193
+ "part_counts": {}, # {part_count: frequency}
194
+ "last_part_count": None,
195
+ }
196
+
197
+ def _init_vertex_ai(self):
198
+ """Lazy initialization of Vertex AI with proper authentication"""
199
+ if not self._initialized:
200
+ if not self.settings.vertex_project_id:
201
+ raise ProviderNotAvailableError("Vertex AI project ID not configured")
202
+
203
+ try:
204
+ # Set up Google Cloud authentication
205
+ pass
206
+
207
+ # Check if GOOGLE_APPLICATION_CREDENTIALS is configured
208
+ if self.settings.google_application_credentials:
209
+ credentials_path = self.settings.google_application_credentials
210
+ if os.path.exists(credentials_path):
211
+ # Set the environment variable for Google Cloud SDK
212
+ os.environ["GOOGLE_APPLICATION_CREDENTIALS"] = credentials_path
213
+ self.logger.info(f"Using Google Cloud credentials from: {credentials_path}")
214
+ else:
215
+ self.logger.warning(f"Google Cloud credentials file not found: {credentials_path}")
216
+ raise ProviderNotAvailableError(f"Google Cloud credentials file not found: {credentials_path}")
217
+ elif "GOOGLE_APPLICATION_CREDENTIALS" in os.environ:
218
+ self.logger.info("Using Google Cloud credentials from environment variable")
219
+ else:
220
+ self.logger.warning("No Google Cloud credentials configured. Using default authentication.")
221
+
222
+ # Initialize Vertex AI
223
+ vertexai.init(
224
+ project=self.settings.vertex_project_id,
225
+ location=getattr(self.settings, "vertex_location", "us-central1"),
226
+ )
227
+ self._initialized = True
228
+ self.logger.info(f"Vertex AI initialized for project {self.settings.vertex_project_id}")
229
+
230
+ except Exception as e:
231
+ raise ProviderNotAvailableError(f"Failed to initialize Vertex AI: {str(e)}")
232
+
233
+ def _convert_messages_to_contents(
234
+ self, messages: List[LLMMessage]
235
+ ) -> List[Content]:
236
+ """
237
+ Convert LLMMessage list to Vertex AI Content objects.
238
+
239
+ This properly handles multi-turn conversations instead of
240
+ string concatenation.
241
+
242
+ Args:
243
+ messages: List of LLMMessage objects (system messages should be filtered out)
244
+
245
+ Returns:
246
+ List of Content objects for Vertex AI API
247
+ """
248
+ contents = []
249
+ for msg in messages:
250
+ # Map role: Vertex AI uses "model" for assistant responses
251
+ role = "model" if msg.role == "assistant" else msg.role
252
+ contents.append(Content(
253
+ role=role,
254
+ parts=[Part.from_text(msg.content)]
255
+ ))
256
+ return contents
257
+
258
+ async def generate_text(
259
+ self,
260
+ messages: List[LLMMessage],
261
+ model: Optional[str] = None,
262
+ temperature: float = 0.7,
263
+ max_tokens: Optional[int] = None,
264
+ functions: Optional[List[Dict[str, Any]]] = None,
265
+ tools: Optional[List[Dict[str, Any]]] = None,
266
+ tool_choice: Optional[Any] = None,
267
+ system_instruction: Optional[str] = None,
268
+ **kwargs,
269
+ ) -> LLMResponse:
270
+ """Generate text using Vertex AI"""
271
+ self._init_vertex_ai()
272
+
273
+ # Get model name from config if not provided
274
+ model_name = model or self._get_default_model() or "gemini-2.5-pro"
275
+
276
+ # Get model config for default parameters
277
+ model_config = self._get_model_config(model_name)
278
+ if model_config and max_tokens is None:
279
+ max_tokens = model_config.default_params.max_tokens
280
+
281
+ try:
282
+ # Extract system message from messages if present
283
+ system_msg = None
284
+ user_messages = []
285
+ for msg in messages:
286
+ if msg.role == "system":
287
+ system_msg = msg.content
288
+ else:
289
+ user_messages.append(msg)
290
+
291
+ # Use explicit system_instruction parameter if provided, else use extracted system message
292
+ final_system_instruction = system_instruction or system_msg
293
+
294
+ # Initialize model WITH system instruction for prompt caching support
295
+ model_instance = GenerativeModel(
296
+ model_name,
297
+ system_instruction=final_system_instruction
298
+ )
299
+ self.logger.debug(f"Initialized Vertex AI model: {model_name}")
300
+
301
+ # Convert messages to Vertex AI format
302
+ if len(user_messages) == 1 and user_messages[0].role == "user":
303
+ contents = user_messages[0].content
304
+ else:
305
+ # For multi-turn conversations, use proper Content objects
306
+ contents = self._convert_messages_to_contents(user_messages)
307
+
308
+ # Use modern GenerationConfig object
309
+ generation_config = GenerationConfig(
310
+ temperature=temperature,
311
+ # Increased to account for thinking tokens
312
+ max_output_tokens=max_tokens or 8192,
313
+ top_p=0.95,
314
+ top_k=40,
315
+ )
316
+
317
+ # Modern safety settings configuration using SafetySetting objects
318
+ # Allow override via kwargs, otherwise use defaults (BLOCK_NONE for all categories)
319
+ if "safety_settings" in kwargs:
320
+ safety_settings = kwargs["safety_settings"]
321
+ if not isinstance(safety_settings, list):
322
+ raise ValueError("safety_settings must be a list of SafetySetting objects")
323
+ else:
324
+ # Default safety settings - can be configured via environment or config
325
+ # Default to BLOCK_NONE to allow all content (can be overridden)
326
+ safety_settings = [
327
+ SafetySetting(
328
+ category=HarmCategory.HARM_CATEGORY_HARASSMENT,
329
+ threshold=HarmBlockThreshold.BLOCK_NONE,
330
+ ),
331
+ SafetySetting(
332
+ category=HarmCategory.HARM_CATEGORY_HATE_SPEECH,
333
+ threshold=HarmBlockThreshold.BLOCK_NONE,
334
+ ),
335
+ SafetySetting(
336
+ category=HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
337
+ threshold=HarmBlockThreshold.BLOCK_NONE,
338
+ ),
339
+ SafetySetting(
340
+ category=HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
341
+ threshold=HarmBlockThreshold.BLOCK_NONE,
342
+ ),
343
+ ]
344
+
345
+ # Prepare tools for Function Calling if provided
346
+ tools_for_api = None
347
+ if tools or functions:
348
+ # Convert OpenAI format to Google format
349
+ tools_list = tools or []
350
+ if functions:
351
+ # Convert legacy functions format to tools format
352
+ tools_list = [{"type": "function", "function": func} for func in functions]
353
+
354
+ google_tools = self._convert_openai_to_google_format(tools_list)
355
+ if google_tools:
356
+ tools_for_api = google_tools
357
+
358
+ # Build API call parameters
359
+ api_params = {
360
+ "contents": contents,
361
+ "generation_config": generation_config,
362
+ "safety_settings": safety_settings,
363
+ }
364
+
365
+ # Add tools if available
366
+ if tools_for_api:
367
+ api_params["tools"] = tools_for_api
368
+
369
+ # Add any additional kwargs (but exclude tools/safety_settings to avoid conflicts)
370
+ for key, value in kwargs.items():
371
+ if key not in ["tools", "safety_settings"]:
372
+ api_params[key] = value
373
+
374
+ response = await asyncio.get_event_loop().run_in_executor(
375
+ None,
376
+ lambda: model_instance.generate_content(**api_params),
377
+ )
378
+
379
+ # Check for prompt-level safety blocks first
380
+ if hasattr(response, "prompt_feedback"):
381
+ pf = response.prompt_feedback
382
+ # Check if prompt was blocked
383
+ if hasattr(pf, "block_reason") and pf.block_reason:
384
+ block_reason = str(pf.block_reason)
385
+ if block_reason not in ["BLOCKED_REASON_UNSPECIFIED", "OTHER"]:
386
+ # Prompt was blocked by safety filters
387
+ raise _build_safety_block_error(
388
+ response,
389
+ block_type="prompt",
390
+ default_message="Prompt blocked by safety filters",
391
+ )
392
+ elif hasattr(pf, "get") and pf.get("block_reason"):
393
+ block_reason = pf.get("block_reason")
394
+ if block_reason not in ["BLOCKED_REASON_UNSPECIFIED", "OTHER"]:
395
+ raise _build_safety_block_error(
396
+ response,
397
+ block_type="prompt",
398
+ default_message="Prompt blocked by safety filters",
399
+ )
400
+
401
+ # Handle response content safely - improved multi-part response
402
+ # handling
403
+ content = None
404
+ try:
405
+ # First try to get text directly
406
+ content = response.text
407
+ self.logger.debug(f"Vertex AI response received: {content[:100]}...")
408
+ except (ValueError, AttributeError) as ve:
409
+ # Handle multi-part responses and other issues
410
+ self.logger.warning(f"Cannot get response text directly: {str(ve)}")
411
+
412
+ # Try to extract content from candidates with multi-part
413
+ # support
414
+ if hasattr(response, "candidates") and response.candidates:
415
+ candidate = response.candidates[0]
416
+ self.logger.debug(f"Candidate finish_reason: {getattr(candidate, 'finish_reason', 'unknown')}")
417
+
418
+ # Handle multi-part content
419
+ if hasattr(candidate, "content") and hasattr(candidate.content, "parts"):
420
+ try:
421
+ # Extract text from all parts
422
+ text_parts: List[str] = []
423
+ for part in candidate.content.parts:
424
+ if hasattr(part, "text") and part.text:
425
+ text_parts.append(str(part.text))
426
+
427
+ if text_parts:
428
+ # Log part count for monitoring
429
+ part_count = len(text_parts)
430
+ self.logger.info(f"📊 Vertex AI response: {part_count} parts detected")
431
+
432
+ # Update statistics
433
+ self._part_count_stats["total_responses"] += 1
434
+ self._part_count_stats["part_counts"][part_count] = self._part_count_stats["part_counts"].get(part_count, 0) + 1
435
+ self._part_count_stats["last_part_count"] = part_count
436
+
437
+ # Log statistics if significant variation
438
+ # detected
439
+ if part_count != self._part_count_stats.get("last_part_count", part_count):
440
+ self.logger.warning(f"⚠️ Part count variation detected: {part_count} parts (previous: {self._part_count_stats.get('last_part_count', 'unknown')})")
441
+
442
+ # Handle multi-part response format
443
+ if len(text_parts) > 1:
444
+ # Multi-part response
445
+ # Minimal fix: only fix incomplete <thinking> tags, preserve original order
446
+ # Do NOT reorganize content - let
447
+ # downstream code handle semantics
448
+
449
+ processed_parts = []
450
+ fixed_count = 0
451
+
452
+ for i, part_raw in enumerate(text_parts):
453
+ # Check for thinking content that needs
454
+ # formatting
455
+ needs_thinking_format = False
456
+ # Ensure part is a string (use different name to avoid redefinition)
457
+ part_str: str = str(part_raw) if not isinstance(part_raw, str) else part_raw
458
+
459
+ if "<thinking>" in part_str and "</thinking>" not in part_str: # type: ignore[operator]
460
+ # Incomplete <thinking> tag: add
461
+ # closing tag
462
+ part_str = part_str + "\n</thinking>" # type: ignore[operator]
463
+ needs_thinking_format = True
464
+ self.logger.debug(f" Part {i+1}: Incomplete <thinking> tag fixed")
465
+ elif isinstance(part_str, str) and part_str.startswith("thinking") and "</thinking>" not in part_str: # type: ignore[operator]
466
+ # thinking\n format: convert to
467
+ # <thinking>...</thinking>
468
+ if part_str.startswith("thinking\n"):
469
+ # thinking\n格式:提取内容并包装
470
+ # 跳过 "thinking\n"
471
+ content = part_str[8:]
472
+ else:
473
+ # thinking开头但无换行:提取内容并包装
474
+ # 跳过 "thinking"
475
+ content = part_str[7:]
476
+
477
+ part_str = f"<thinking>\n{content}\n</thinking>"
478
+ needs_thinking_format = True
479
+ self.logger.debug(f" Part {i+1}: thinking\\n format converted to <thinking> tags")
480
+
481
+ if needs_thinking_format:
482
+ fixed_count += 1
483
+
484
+ processed_parts.append(part_str)
485
+
486
+ # Merge in original order
487
+ content = "\n".join(processed_parts)
488
+
489
+ if fixed_count > 0:
490
+ self.logger.info(f"✅ Multi-part response merged: {len(text_parts)} parts, {fixed_count} incomplete tags fixed, order preserved")
491
+ else:
492
+ self.logger.info(f"✅ Multi-part response merged: {len(text_parts)} parts, order preserved")
493
+ else:
494
+ # Single part response - use as is
495
+ content = text_parts[0]
496
+ self.logger.info("Successfully extracted single-part response")
497
+ else:
498
+ self.logger.warning("No text content found in multi-part response")
499
+ except Exception as part_error:
500
+ self.logger.error(f"Failed to extract content from multi-part response: {str(part_error)}")
501
+
502
+ # If still no content, check finish reason
503
+ if not content:
504
+ if hasattr(candidate, "finish_reason"):
505
+ if candidate.finish_reason == "MAX_TOKENS":
506
+ content = "[Response truncated due to token limit - consider increasing max_tokens for Gemini 2.5 models]"
507
+ self.logger.warning("Response truncated due to MAX_TOKENS - Gemini 2.5 uses thinking tokens")
508
+ elif candidate.finish_reason in [
509
+ "SAFETY",
510
+ "RECITATION",
511
+ ]:
512
+ # Response was blocked by safety filters
513
+ raise _build_safety_block_error(
514
+ response,
515
+ block_type="response",
516
+ default_message="Response blocked by safety filters",
517
+ )
518
+ else:
519
+ content = f"[Response error: Cannot get response text - {candidate.finish_reason}]"
520
+ else:
521
+ content = "[Response error: Cannot get the response text]"
522
+ else:
523
+ # No candidates found - check if this is due to safety filters
524
+ # Check prompt_feedback for block reason
525
+ if hasattr(response, "prompt_feedback"):
526
+ pf = response.prompt_feedback
527
+ if hasattr(pf, "block_reason") and pf.block_reason:
528
+ block_reason = str(pf.block_reason)
529
+ if block_reason not in ["BLOCKED_REASON_UNSPECIFIED", "OTHER"]:
530
+ raise _build_safety_block_error(
531
+ response,
532
+ block_type="prompt",
533
+ default_message="No candidates found - prompt blocked by safety filters",
534
+ )
535
+ elif hasattr(pf, "get") and pf.get("block_reason"):
536
+ block_reason = pf.get("block_reason")
537
+ if block_reason not in ["BLOCKED_REASON_UNSPECIFIED", "OTHER"]:
538
+ raise _build_safety_block_error(
539
+ response,
540
+ block_type="prompt",
541
+ default_message="No candidates found - prompt blocked by safety filters",
542
+ )
543
+
544
+ # If not a safety block, raise generic error with details
545
+ error_msg = f"Response error: No candidates found - Response has no candidates (and thus no text)."
546
+ if hasattr(response, "prompt_feedback"):
547
+ error_msg += " Check prompt_feedback for details."
548
+ raise ValueError(error_msg)
549
+
550
+ # Final fallback
551
+ if not content:
552
+ content = "[Response error: Cannot get the response text. Multiple content parts are not supported.]"
553
+
554
+ # Vertex AI doesn't provide detailed token usage in the response
555
+ # Use estimation method as fallback
556
+ input_tokens = self._count_tokens_estimate(prompt)
557
+ output_tokens = self._count_tokens_estimate(content)
558
+ tokens_used = input_tokens + output_tokens
559
+
560
+ # Extract cache metadata from Vertex AI response if available
561
+ cache_read_tokens = None
562
+ cache_hit = None
563
+ if hasattr(response, "usage_metadata"):
564
+ usage = response.usage_metadata
565
+ if hasattr(usage, "cached_content_token_count"):
566
+ cache_read_tokens = usage.cached_content_token_count
567
+ cache_hit = cache_read_tokens is not None and cache_read_tokens > 0
568
+
569
+ # Use config-based cost estimation
570
+ cost = self._estimate_cost_from_config(model_name, input_tokens, output_tokens)
571
+
572
+ # Extract function calls from response if present
573
+ function_calls = self._extract_function_calls_from_google_response(response)
574
+
575
+ llm_response = LLMResponse(
576
+ content=content,
577
+ provider=self.provider_name,
578
+ model=model_name,
579
+ tokens_used=tokens_used,
580
+ prompt_tokens=input_tokens, # Estimated value since Vertex AI doesn't provide detailed usage
581
+ completion_tokens=output_tokens, # Estimated value since Vertex AI doesn't provide detailed usage
582
+ cost_estimate=cost,
583
+ cache_read_tokens=cache_read_tokens,
584
+ cache_hit=cache_hit,
585
+ )
586
+
587
+ # Attach function call info if present
588
+ if function_calls:
589
+ self._attach_function_calls_to_response(llm_response, function_calls)
590
+
591
+ return llm_response
592
+
593
+ except SafetyBlockError:
594
+ # Re-raise safety block errors as-is (they already contain detailed information)
595
+ raise
596
+ except Exception as e:
597
+ if "quota" in str(e).lower() or "limit" in str(e).lower():
598
+ raise RateLimitError(f"Vertex AI quota exceeded: {str(e)}")
599
+ # Handle specific Vertex AI response errors
600
+ if any(
601
+ keyword in str(e).lower()
602
+ for keyword in [
603
+ "cannot get the response text",
604
+ "safety filters",
605
+ "multiple content parts are not supported",
606
+ "cannot get the candidate text",
607
+ ]
608
+ ):
609
+ self.logger.warning(f"Vertex AI response issue: {str(e)}")
610
+ # Return a response indicating the issue
611
+ estimated_prompt_tokens = self._count_tokens_estimate(prompt)
612
+ return LLMResponse(
613
+ content="[Response unavailable due to content processing issues or safety filters]",
614
+ provider=self.provider_name,
615
+ model=model_name,
616
+ tokens_used=estimated_prompt_tokens,
617
+ prompt_tokens=estimated_prompt_tokens,
618
+ completion_tokens=0,
619
+ cost_estimate=0.0,
620
+ )
621
+ raise
622
+
623
+ async def stream_text( # type: ignore[override]
624
+ self,
625
+ messages: List[LLMMessage],
626
+ model: Optional[str] = None,
627
+ temperature: float = 0.7,
628
+ max_tokens: Optional[int] = None,
629
+ functions: Optional[List[Dict[str, Any]]] = None,
630
+ tools: Optional[List[Dict[str, Any]]] = None,
631
+ tool_choice: Optional[Any] = None,
632
+ return_chunks: bool = False,
633
+ system_instruction: Optional[str] = None,
634
+ **kwargs,
635
+ ) -> AsyncGenerator[Any, None]:
636
+ """
637
+ Stream text using Vertex AI real streaming API with Function Calling support.
638
+
639
+ Args:
640
+ messages: List of LLM messages
641
+ model: Model name (optional)
642
+ temperature: Temperature for generation
643
+ max_tokens: Maximum tokens to generate
644
+ functions: List of function schemas (legacy format)
645
+ tools: List of tool schemas (new format)
646
+ tool_choice: Tool choice strategy (not used for Google Vertex AI)
647
+ return_chunks: If True, returns GoogleStreamChunk objects; if False, returns str tokens only
648
+ system_instruction: System instruction for prompt caching support
649
+ **kwargs: Additional arguments
650
+
651
+ Yields:
652
+ str or GoogleStreamChunk: Text tokens or StreamChunk objects
653
+ """
654
+ self._init_vertex_ai()
655
+
656
+ # Get model name from config if not provided
657
+ model_name = model or self._get_default_model() or "gemini-2.5-pro"
658
+
659
+ # Get model config for default parameters
660
+ model_config = self._get_model_config(model_name)
661
+ if model_config and max_tokens is None:
662
+ max_tokens = model_config.default_params.max_tokens
663
+
664
+ try:
665
+ # Extract system message from messages if present
666
+ system_msg = None
667
+ user_messages = []
668
+ for msg in messages:
669
+ if msg.role == "system":
670
+ system_msg = msg.content
671
+ else:
672
+ user_messages.append(msg)
673
+
674
+ # Use explicit system_instruction parameter if provided, else use extracted system message
675
+ final_system_instruction = system_instruction or system_msg
676
+
677
+ # Initialize model WITH system instruction for prompt caching support
678
+ model_instance = GenerativeModel(
679
+ model_name,
680
+ system_instruction=final_system_instruction
681
+ )
682
+ self.logger.debug(f"Initialized Vertex AI model for streaming: {model_name}")
683
+
684
+ # Convert messages to Vertex AI format
685
+ if len(user_messages) == 1 and user_messages[0].role == "user":
686
+ contents = user_messages[0].content
687
+ else:
688
+ # For multi-turn conversations, use proper Content objects
689
+ contents = self._convert_messages_to_contents(user_messages)
690
+
691
+ # Use modern GenerationConfig object
692
+ generation_config = GenerationConfig(
693
+ temperature=temperature,
694
+ max_output_tokens=max_tokens or 8192,
695
+ top_p=0.95,
696
+ top_k=40,
697
+ )
698
+
699
+ # Get safety settings from kwargs or use defaults
700
+ if "safety_settings" in kwargs:
701
+ safety_settings = kwargs["safety_settings"]
702
+ if not isinstance(safety_settings, list):
703
+ raise ValueError("safety_settings must be a list of SafetySetting objects")
704
+ else:
705
+ # Default safety settings
706
+ safety_settings = [
707
+ SafetySetting(
708
+ category=HarmCategory.HARM_CATEGORY_HARASSMENT,
709
+ threshold=HarmBlockThreshold.BLOCK_NONE,
710
+ ),
711
+ SafetySetting(
712
+ category=HarmCategory.HARM_CATEGORY_HATE_SPEECH,
713
+ threshold=HarmBlockThreshold.BLOCK_NONE,
714
+ ),
715
+ SafetySetting(
716
+ category=HarmCategory.HARM_CATEGORY_SEXUALLY_EXPLICIT,
717
+ threshold=HarmBlockThreshold.BLOCK_NONE,
718
+ ),
719
+ SafetySetting(
720
+ category=HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT,
721
+ threshold=HarmBlockThreshold.BLOCK_NONE,
722
+ ),
723
+ ]
724
+
725
+ # Prepare tools for Function Calling if provided
726
+ tools_for_api = None
727
+ if tools or functions:
728
+ # Convert OpenAI format to Google format
729
+ tools_list = tools or []
730
+ if functions:
731
+ # Convert legacy functions format to tools format
732
+ tools_list = [{"type": "function", "function": func} for func in functions]
733
+
734
+ google_tools = self._convert_openai_to_google_format(tools_list)
735
+ if google_tools:
736
+ tools_for_api = google_tools
737
+
738
+ # Use mixin method for Function Calling support
739
+ from aiecs.llm.clients.openai_compatible_mixin import StreamChunk
740
+
741
+ async for chunk in self._stream_text_with_function_calling(
742
+ model_instance=model_instance,
743
+ contents=contents,
744
+ generation_config=generation_config,
745
+ safety_settings=safety_settings,
746
+ tools=tools_for_api,
747
+ return_chunks=return_chunks,
748
+ **kwargs,
749
+ ):
750
+ # Yield chunk (can be str or StreamChunk)
751
+ yield chunk
752
+
753
+ except SafetyBlockError:
754
+ # Re-raise safety block errors as-is
755
+ raise
756
+ except Exception as e:
757
+ if "quota" in str(e).lower() or "limit" in str(e).lower():
758
+ raise RateLimitError(f"Vertex AI quota exceeded: {str(e)}")
759
+ self.logger.error(f"Error in Vertex AI streaming: {str(e)}")
760
+ raise
761
+
762
+ def get_part_count_stats(self) -> Dict[str, Any]:
763
+ """
764
+ Get statistics about part count variations in Vertex AI responses.
765
+
766
+ Returns:
767
+ Dictionary containing part count statistics and analysis
768
+ """
769
+ stats = self._part_count_stats.copy()
770
+
771
+ if stats["total_responses"] > 0:
772
+ # Calculate variation metrics
773
+ part_counts = list(stats["part_counts"].keys())
774
+ stats["variation_analysis"] = {
775
+ "unique_part_counts": len(part_counts),
776
+ "most_common_count": (max(stats["part_counts"].items(), key=lambda x: x[1])[0] if stats["part_counts"] else None),
777
+ "part_count_range": (f"{min(part_counts)}-{max(part_counts)}" if part_counts else "N/A"),
778
+ # 0-1, higher is more stable
779
+ "stability_score": 1.0 - (len(part_counts) - 1) / max(stats["total_responses"], 1),
780
+ }
781
+
782
+ # Generate recommendations
783
+ if stats["variation_analysis"]["stability_score"] < 0.7:
784
+ stats["recommendations"] = [
785
+ "High part count variation detected",
786
+ "Consider optimizing prompt structure",
787
+ "Monitor input complexity patterns",
788
+ "Review tool calling configuration",
789
+ ]
790
+ else:
791
+ stats["recommendations"] = [
792
+ "Part count variation is within acceptable range",
793
+ "Continue monitoring for patterns",
794
+ ]
795
+
796
+ return stats
797
+
798
+ def log_part_count_summary(self):
799
+ """Log a summary of part count statistics"""
800
+ stats = self.get_part_count_stats()
801
+
802
+ if stats["total_responses"] > 0:
803
+ self.logger.info("📈 Vertex AI Part Count Summary:")
804
+ self.logger.info(f" Total responses: {stats['total_responses']}")
805
+ self.logger.info(f" Part count distribution: {stats['part_counts']}")
806
+
807
+ if "variation_analysis" in stats:
808
+ analysis = stats["variation_analysis"]
809
+ self.logger.info(f" Stability score: {analysis['stability_score']:.2f}")
810
+ self.logger.info(f" Most common count: {analysis['most_common_count']}")
811
+ self.logger.info(f" Count range: {analysis['part_count_range']}")
812
+
813
+ if "recommendations" in stats:
814
+ self.logger.info(" Recommendations:")
815
+ for rec in stats["recommendations"]:
816
+ self.logger.info(f" • {rec}")
817
+
818
+ async def get_embeddings(
819
+ self,
820
+ texts: List[str],
821
+ model: Optional[str] = None,
822
+ ) -> List[List[float]]:
823
+ """
824
+ Generate embeddings using Vertex AI embedding model
825
+
826
+ Args:
827
+ texts: List of texts to embed
828
+ model: Embedding model name (default: gemini-embedding-001)
829
+
830
+ Returns:
831
+ List of embedding vectors (each is a list of floats)
832
+ """
833
+ self._init_vertex_ai()
834
+
835
+ # Use gemini-embedding-001 as default
836
+ embedding_model_name = model or "gemini-embedding-001"
837
+
838
+ try:
839
+ from google.cloud import aiplatform
840
+ from google.cloud.aiplatform.gapic import PredictionServiceClient
841
+ from google.protobuf import struct_pb2
842
+
843
+ # Initialize prediction client
844
+ location = getattr(self.settings, "vertex_location", "us-central1")
845
+ endpoint = f"{location}-aiplatform.googleapis.com"
846
+ client = PredictionServiceClient(client_options={"api_endpoint": endpoint})
847
+
848
+ # Model resource name
849
+ model_resource = f"projects/{self.settings.vertex_project_id}/locations/{location}/publishers/google/models/{embedding_model_name}"
850
+
851
+ # Generate embeddings for each text
852
+ embeddings = []
853
+ for text in texts:
854
+ # Prepare instance
855
+ instance = struct_pb2.Struct()
856
+ instance.fields["content"].string_value = text
857
+
858
+ # Make prediction request
859
+ response = await asyncio.get_event_loop().run_in_executor(
860
+ None,
861
+ lambda: client.predict(
862
+ endpoint=model_resource,
863
+ instances=[instance]
864
+ )
865
+ )
866
+
867
+ # Extract embedding
868
+ if response.predictions and len(response.predictions) > 0:
869
+ prediction = response.predictions[0]
870
+ if "embeddings" in prediction and "values" in prediction["embeddings"]:
871
+ embedding = list(prediction["embeddings"]["values"])
872
+ embeddings.append(embedding)
873
+ else:
874
+ self.logger.warning(f"Unexpected response format for embedding: {prediction}")
875
+ # Return zero vector as fallback
876
+ embeddings.append([0.0] * 768)
877
+ else:
878
+ self.logger.warning("No predictions returned from embedding model")
879
+ embeddings.append([0.0] * 768)
880
+
881
+ return embeddings
882
+
883
+ except ImportError as e:
884
+ self.logger.error(f"Required Vertex AI libraries not available: {e}")
885
+ # Return zero vectors as fallback
886
+ return [[0.0] * 768 for _ in texts]
887
+ except Exception as e:
888
+ self.logger.error(f"Error generating embeddings with Vertex AI: {e}")
889
+ # Return zero vectors as fallback
890
+ return [[0.0] * 768 for _ in texts]
891
+
892
+ async def close(self):
893
+ """Clean up resources"""
894
+ # Log final statistics before cleanup
895
+ self.log_part_count_summary()
896
+ # Vertex AI doesn't require explicit cleanup
897
+ self._initialized = False