aiecs 1.0.1__py3-none-any.whl → 1.7.17__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.
- aiecs/__init__.py +13 -16
- aiecs/__main__.py +7 -7
- aiecs/aiecs_client.py +269 -75
- aiecs/application/executors/operation_executor.py +79 -54
- aiecs/application/knowledge_graph/__init__.py +7 -0
- aiecs/application/knowledge_graph/builder/__init__.py +37 -0
- aiecs/application/knowledge_graph/builder/data_quality.py +302 -0
- aiecs/application/knowledge_graph/builder/data_reshaping.py +293 -0
- aiecs/application/knowledge_graph/builder/document_builder.py +369 -0
- aiecs/application/knowledge_graph/builder/graph_builder.py +490 -0
- aiecs/application/knowledge_graph/builder/import_optimizer.py +396 -0
- aiecs/application/knowledge_graph/builder/schema_inference.py +462 -0
- aiecs/application/knowledge_graph/builder/schema_mapping.py +563 -0
- aiecs/application/knowledge_graph/builder/structured_pipeline.py +1384 -0
- aiecs/application/knowledge_graph/builder/text_chunker.py +317 -0
- aiecs/application/knowledge_graph/extractors/__init__.py +27 -0
- aiecs/application/knowledge_graph/extractors/base.py +98 -0
- aiecs/application/knowledge_graph/extractors/llm_entity_extractor.py +422 -0
- aiecs/application/knowledge_graph/extractors/llm_relation_extractor.py +347 -0
- aiecs/application/knowledge_graph/extractors/ner_entity_extractor.py +241 -0
- aiecs/application/knowledge_graph/fusion/__init__.py +78 -0
- aiecs/application/knowledge_graph/fusion/ab_testing.py +395 -0
- aiecs/application/knowledge_graph/fusion/abbreviation_expander.py +327 -0
- aiecs/application/knowledge_graph/fusion/alias_index.py +597 -0
- aiecs/application/knowledge_graph/fusion/alias_matcher.py +384 -0
- aiecs/application/knowledge_graph/fusion/cache_coordinator.py +343 -0
- aiecs/application/knowledge_graph/fusion/entity_deduplicator.py +433 -0
- aiecs/application/knowledge_graph/fusion/entity_linker.py +511 -0
- aiecs/application/knowledge_graph/fusion/evaluation_dataset.py +240 -0
- aiecs/application/knowledge_graph/fusion/knowledge_fusion.py +632 -0
- aiecs/application/knowledge_graph/fusion/matching_config.py +489 -0
- aiecs/application/knowledge_graph/fusion/name_normalizer.py +352 -0
- aiecs/application/knowledge_graph/fusion/relation_deduplicator.py +183 -0
- aiecs/application/knowledge_graph/fusion/semantic_name_matcher.py +464 -0
- aiecs/application/knowledge_graph/fusion/similarity_pipeline.py +534 -0
- aiecs/application/knowledge_graph/pattern_matching/__init__.py +21 -0
- aiecs/application/knowledge_graph/pattern_matching/pattern_matcher.py +342 -0
- aiecs/application/knowledge_graph/pattern_matching/query_executor.py +366 -0
- aiecs/application/knowledge_graph/profiling/__init__.py +12 -0
- aiecs/application/knowledge_graph/profiling/query_plan_visualizer.py +195 -0
- aiecs/application/knowledge_graph/profiling/query_profiler.py +223 -0
- aiecs/application/knowledge_graph/reasoning/__init__.py +27 -0
- aiecs/application/knowledge_graph/reasoning/evidence_synthesis.py +341 -0
- aiecs/application/knowledge_graph/reasoning/inference_engine.py +500 -0
- aiecs/application/knowledge_graph/reasoning/logic_form_parser.py +163 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/__init__.py +79 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/ast_builder.py +513 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/ast_nodes.py +913 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/ast_validator.py +866 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/error_handler.py +475 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/parser.py +396 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/query_context.py +208 -0
- aiecs/application/knowledge_graph/reasoning/logic_query_integration.py +170 -0
- aiecs/application/knowledge_graph/reasoning/query_planner.py +855 -0
- aiecs/application/knowledge_graph/reasoning/reasoning_engine.py +518 -0
- aiecs/application/knowledge_graph/retrieval/__init__.py +27 -0
- aiecs/application/knowledge_graph/retrieval/query_intent_classifier.py +211 -0
- aiecs/application/knowledge_graph/retrieval/retrieval_strategies.py +592 -0
- aiecs/application/knowledge_graph/retrieval/strategy_types.py +23 -0
- aiecs/application/knowledge_graph/search/__init__.py +59 -0
- aiecs/application/knowledge_graph/search/hybrid_search.py +457 -0
- aiecs/application/knowledge_graph/search/reranker.py +293 -0
- aiecs/application/knowledge_graph/search/reranker_strategies.py +535 -0
- aiecs/application/knowledge_graph/search/text_similarity.py +392 -0
- aiecs/application/knowledge_graph/traversal/__init__.py +15 -0
- aiecs/application/knowledge_graph/traversal/enhanced_traversal.py +305 -0
- aiecs/application/knowledge_graph/traversal/path_scorer.py +271 -0
- aiecs/application/knowledge_graph/validators/__init__.py +13 -0
- aiecs/application/knowledge_graph/validators/relation_validator.py +239 -0
- aiecs/application/knowledge_graph/visualization/__init__.py +11 -0
- aiecs/application/knowledge_graph/visualization/graph_visualizer.py +313 -0
- aiecs/common/__init__.py +9 -0
- aiecs/common/knowledge_graph/__init__.py +17 -0
- aiecs/common/knowledge_graph/runnable.py +471 -0
- aiecs/config/__init__.py +20 -5
- aiecs/config/config.py +762 -31
- aiecs/config/graph_config.py +131 -0
- aiecs/config/tool_config.py +435 -0
- aiecs/core/__init__.py +29 -13
- aiecs/core/interface/__init__.py +2 -2
- aiecs/core/interface/execution_interface.py +22 -22
- aiecs/core/interface/storage_interface.py +37 -88
- aiecs/core/registry/__init__.py +31 -0
- aiecs/core/registry/service_registry.py +92 -0
- aiecs/domain/__init__.py +270 -1
- aiecs/domain/agent/__init__.py +191 -0
- aiecs/domain/agent/base_agent.py +3949 -0
- aiecs/domain/agent/exceptions.py +99 -0
- aiecs/domain/agent/graph_aware_mixin.py +569 -0
- aiecs/domain/agent/hybrid_agent.py +1731 -0
- aiecs/domain/agent/integration/__init__.py +29 -0
- aiecs/domain/agent/integration/context_compressor.py +216 -0
- aiecs/domain/agent/integration/context_engine_adapter.py +587 -0
- aiecs/domain/agent/integration/protocols.py +281 -0
- aiecs/domain/agent/integration/retry_policy.py +218 -0
- aiecs/domain/agent/integration/role_config.py +213 -0
- aiecs/domain/agent/knowledge_aware_agent.py +1892 -0
- aiecs/domain/agent/lifecycle.py +291 -0
- aiecs/domain/agent/llm_agent.py +692 -0
- aiecs/domain/agent/memory/__init__.py +12 -0
- aiecs/domain/agent/memory/conversation.py +1124 -0
- aiecs/domain/agent/migration/__init__.py +14 -0
- aiecs/domain/agent/migration/conversion.py +163 -0
- aiecs/domain/agent/migration/legacy_wrapper.py +86 -0
- aiecs/domain/agent/models.py +894 -0
- aiecs/domain/agent/observability.py +479 -0
- aiecs/domain/agent/persistence.py +449 -0
- aiecs/domain/agent/prompts/__init__.py +29 -0
- aiecs/domain/agent/prompts/builder.py +159 -0
- aiecs/domain/agent/prompts/formatters.py +187 -0
- aiecs/domain/agent/prompts/template.py +255 -0
- aiecs/domain/agent/registry.py +253 -0
- aiecs/domain/agent/tool_agent.py +444 -0
- aiecs/domain/agent/tools/__init__.py +15 -0
- aiecs/domain/agent/tools/schema_generator.py +377 -0
- aiecs/domain/community/__init__.py +155 -0
- aiecs/domain/community/agent_adapter.py +469 -0
- aiecs/domain/community/analytics.py +432 -0
- aiecs/domain/community/collaborative_workflow.py +648 -0
- aiecs/domain/community/communication_hub.py +634 -0
- aiecs/domain/community/community_builder.py +320 -0
- aiecs/domain/community/community_integration.py +796 -0
- aiecs/domain/community/community_manager.py +803 -0
- aiecs/domain/community/decision_engine.py +849 -0
- aiecs/domain/community/exceptions.py +231 -0
- aiecs/domain/community/models/__init__.py +33 -0
- aiecs/domain/community/models/community_models.py +234 -0
- aiecs/domain/community/resource_manager.py +461 -0
- aiecs/domain/community/shared_context_manager.py +589 -0
- aiecs/domain/context/__init__.py +40 -10
- aiecs/domain/context/context_engine.py +1910 -0
- aiecs/domain/context/conversation_models.py +87 -53
- aiecs/domain/context/graph_memory.py +582 -0
- aiecs/domain/execution/model.py +12 -4
- aiecs/domain/knowledge_graph/__init__.py +19 -0
- aiecs/domain/knowledge_graph/models/__init__.py +52 -0
- aiecs/domain/knowledge_graph/models/entity.py +148 -0
- aiecs/domain/knowledge_graph/models/evidence.py +178 -0
- aiecs/domain/knowledge_graph/models/inference_rule.py +184 -0
- aiecs/domain/knowledge_graph/models/path.py +171 -0
- aiecs/domain/knowledge_graph/models/path_pattern.py +171 -0
- aiecs/domain/knowledge_graph/models/query.py +261 -0
- aiecs/domain/knowledge_graph/models/query_plan.py +181 -0
- aiecs/domain/knowledge_graph/models/relation.py +202 -0
- aiecs/domain/knowledge_graph/schema/__init__.py +23 -0
- aiecs/domain/knowledge_graph/schema/entity_type.py +131 -0
- aiecs/domain/knowledge_graph/schema/graph_schema.py +253 -0
- aiecs/domain/knowledge_graph/schema/property_schema.py +143 -0
- aiecs/domain/knowledge_graph/schema/relation_type.py +163 -0
- aiecs/domain/knowledge_graph/schema/schema_manager.py +691 -0
- aiecs/domain/knowledge_graph/schema/type_enums.py +209 -0
- aiecs/domain/task/dsl_processor.py +172 -56
- aiecs/domain/task/model.py +20 -8
- aiecs/domain/task/task_context.py +27 -24
- aiecs/infrastructure/__init__.py +0 -2
- aiecs/infrastructure/graph_storage/__init__.py +11 -0
- aiecs/infrastructure/graph_storage/base.py +837 -0
- aiecs/infrastructure/graph_storage/batch_operations.py +458 -0
- aiecs/infrastructure/graph_storage/cache.py +424 -0
- aiecs/infrastructure/graph_storage/distributed.py +223 -0
- aiecs/infrastructure/graph_storage/error_handling.py +380 -0
- aiecs/infrastructure/graph_storage/graceful_degradation.py +294 -0
- aiecs/infrastructure/graph_storage/health_checks.py +378 -0
- aiecs/infrastructure/graph_storage/in_memory.py +1197 -0
- aiecs/infrastructure/graph_storage/index_optimization.py +446 -0
- aiecs/infrastructure/graph_storage/lazy_loading.py +431 -0
- aiecs/infrastructure/graph_storage/metrics.py +344 -0
- aiecs/infrastructure/graph_storage/migration.py +400 -0
- aiecs/infrastructure/graph_storage/pagination.py +483 -0
- aiecs/infrastructure/graph_storage/performance_monitoring.py +456 -0
- aiecs/infrastructure/graph_storage/postgres.py +1563 -0
- aiecs/infrastructure/graph_storage/property_storage.py +353 -0
- aiecs/infrastructure/graph_storage/protocols.py +76 -0
- aiecs/infrastructure/graph_storage/query_optimizer.py +642 -0
- aiecs/infrastructure/graph_storage/schema_cache.py +290 -0
- aiecs/infrastructure/graph_storage/sqlite.py +1373 -0
- aiecs/infrastructure/graph_storage/streaming.py +487 -0
- aiecs/infrastructure/graph_storage/tenant.py +412 -0
- aiecs/infrastructure/messaging/celery_task_manager.py +92 -54
- aiecs/infrastructure/messaging/websocket_manager.py +51 -35
- aiecs/infrastructure/monitoring/__init__.py +22 -0
- aiecs/infrastructure/monitoring/executor_metrics.py +45 -11
- aiecs/infrastructure/monitoring/global_metrics_manager.py +212 -0
- aiecs/infrastructure/monitoring/structured_logger.py +3 -7
- aiecs/infrastructure/monitoring/tracing_manager.py +63 -35
- aiecs/infrastructure/persistence/__init__.py +14 -1
- aiecs/infrastructure/persistence/context_engine_client.py +184 -0
- aiecs/infrastructure/persistence/database_manager.py +67 -43
- aiecs/infrastructure/persistence/file_storage.py +180 -103
- aiecs/infrastructure/persistence/redis_client.py +74 -21
- aiecs/llm/__init__.py +73 -25
- aiecs/llm/callbacks/__init__.py +11 -0
- aiecs/llm/{custom_callbacks.py → callbacks/custom_callbacks.py} +26 -19
- aiecs/llm/client_factory.py +230 -37
- aiecs/llm/client_resolver.py +155 -0
- aiecs/llm/clients/__init__.py +38 -0
- aiecs/llm/clients/base_client.py +328 -0
- aiecs/llm/clients/google_function_calling_mixin.py +415 -0
- aiecs/llm/clients/googleai_client.py +314 -0
- aiecs/llm/clients/openai_client.py +158 -0
- aiecs/llm/clients/openai_compatible_mixin.py +367 -0
- aiecs/llm/clients/vertex_client.py +1186 -0
- aiecs/llm/clients/xai_client.py +201 -0
- aiecs/llm/config/__init__.py +51 -0
- aiecs/llm/config/config_loader.py +272 -0
- aiecs/llm/config/config_validator.py +206 -0
- aiecs/llm/config/model_config.py +143 -0
- aiecs/llm/protocols.py +149 -0
- aiecs/llm/utils/__init__.py +10 -0
- aiecs/llm/utils/validate_config.py +89 -0
- aiecs/main.py +140 -121
- aiecs/scripts/aid/VERSION_MANAGEMENT.md +138 -0
- aiecs/scripts/aid/__init__.py +19 -0
- aiecs/scripts/aid/module_checker.py +499 -0
- aiecs/scripts/aid/version_manager.py +235 -0
- aiecs/scripts/{DEPENDENCY_SYSTEM_SUMMARY.md → dependance_check/DEPENDENCY_SYSTEM_SUMMARY.md} +1 -0
- aiecs/scripts/{README_DEPENDENCY_CHECKER.md → dependance_check/README_DEPENDENCY_CHECKER.md} +1 -0
- aiecs/scripts/dependance_check/__init__.py +15 -0
- aiecs/scripts/dependance_check/dependency_checker.py +1835 -0
- aiecs/scripts/{dependency_fixer.py → dependance_check/dependency_fixer.py} +192 -90
- aiecs/scripts/{download_nlp_data.py → dependance_check/download_nlp_data.py} +203 -71
- aiecs/scripts/dependance_patch/__init__.py +7 -0
- aiecs/scripts/dependance_patch/fix_weasel/__init__.py +11 -0
- aiecs/scripts/{fix_weasel_validator.py → dependance_patch/fix_weasel/fix_weasel_validator.py} +21 -14
- aiecs/scripts/{patch_weasel_library.sh → dependance_patch/fix_weasel/patch_weasel_library.sh} +1 -1
- aiecs/scripts/knowledge_graph/__init__.py +3 -0
- aiecs/scripts/knowledge_graph/run_threshold_experiments.py +212 -0
- aiecs/scripts/migrations/multi_tenancy/README.md +142 -0
- aiecs/scripts/tools_develop/README.md +671 -0
- aiecs/scripts/tools_develop/README_CONFIG_CHECKER.md +273 -0
- aiecs/scripts/tools_develop/TOOLS_CONFIG_GUIDE.md +1287 -0
- aiecs/scripts/tools_develop/TOOL_AUTO_DISCOVERY.md +234 -0
- aiecs/scripts/tools_develop/__init__.py +21 -0
- aiecs/scripts/tools_develop/check_all_tools_config.py +548 -0
- aiecs/scripts/tools_develop/check_type_annotations.py +257 -0
- aiecs/scripts/tools_develop/pre-commit-schema-coverage.sh +66 -0
- aiecs/scripts/tools_develop/schema_coverage.py +511 -0
- aiecs/scripts/tools_develop/validate_tool_schemas.py +475 -0
- aiecs/scripts/tools_develop/verify_executor_config_fix.py +98 -0
- aiecs/scripts/tools_develop/verify_tools.py +352 -0
- aiecs/tasks/__init__.py +0 -1
- aiecs/tasks/worker.py +115 -47
- aiecs/tools/__init__.py +194 -72
- aiecs/tools/apisource/__init__.py +99 -0
- aiecs/tools/apisource/intelligence/__init__.py +19 -0
- aiecs/tools/apisource/intelligence/data_fusion.py +632 -0
- aiecs/tools/apisource/intelligence/query_analyzer.py +417 -0
- aiecs/tools/apisource/intelligence/search_enhancer.py +385 -0
- aiecs/tools/apisource/monitoring/__init__.py +9 -0
- aiecs/tools/apisource/monitoring/metrics.py +330 -0
- aiecs/tools/apisource/providers/__init__.py +112 -0
- aiecs/tools/apisource/providers/base.py +671 -0
- aiecs/tools/apisource/providers/census.py +397 -0
- aiecs/tools/apisource/providers/fred.py +535 -0
- aiecs/tools/apisource/providers/newsapi.py +409 -0
- aiecs/tools/apisource/providers/worldbank.py +352 -0
- aiecs/tools/apisource/reliability/__init__.py +12 -0
- aiecs/tools/apisource/reliability/error_handler.py +363 -0
- aiecs/tools/apisource/reliability/fallback_strategy.py +376 -0
- aiecs/tools/apisource/tool.py +832 -0
- aiecs/tools/apisource/utils/__init__.py +9 -0
- aiecs/tools/apisource/utils/validators.py +334 -0
- aiecs/tools/base_tool.py +415 -21
- aiecs/tools/docs/__init__.py +121 -0
- aiecs/tools/docs/ai_document_orchestrator.py +607 -0
- aiecs/tools/docs/ai_document_writer_orchestrator.py +2350 -0
- aiecs/tools/docs/content_insertion_tool.py +1320 -0
- aiecs/tools/docs/document_creator_tool.py +1464 -0
- aiecs/tools/docs/document_layout_tool.py +1160 -0
- aiecs/tools/docs/document_parser_tool.py +1016 -0
- aiecs/tools/docs/document_writer_tool.py +2008 -0
- aiecs/tools/knowledge_graph/__init__.py +17 -0
- aiecs/tools/knowledge_graph/graph_reasoning_tool.py +807 -0
- aiecs/tools/knowledge_graph/graph_search_tool.py +944 -0
- aiecs/tools/knowledge_graph/kg_builder_tool.py +524 -0
- aiecs/tools/langchain_adapter.py +300 -138
- aiecs/tools/schema_generator.py +455 -0
- aiecs/tools/search_tool/__init__.py +100 -0
- aiecs/tools/search_tool/analyzers.py +581 -0
- aiecs/tools/search_tool/cache.py +264 -0
- aiecs/tools/search_tool/constants.py +128 -0
- aiecs/tools/search_tool/context.py +224 -0
- aiecs/tools/search_tool/core.py +778 -0
- aiecs/tools/search_tool/deduplicator.py +119 -0
- aiecs/tools/search_tool/error_handler.py +242 -0
- aiecs/tools/search_tool/metrics.py +343 -0
- aiecs/tools/search_tool/rate_limiter.py +172 -0
- aiecs/tools/search_tool/schemas.py +275 -0
- aiecs/tools/statistics/__init__.py +80 -0
- aiecs/tools/statistics/ai_data_analysis_orchestrator.py +646 -0
- aiecs/tools/statistics/ai_insight_generator_tool.py +508 -0
- aiecs/tools/statistics/ai_report_orchestrator_tool.py +684 -0
- aiecs/tools/statistics/data_loader_tool.py +555 -0
- aiecs/tools/statistics/data_profiler_tool.py +638 -0
- aiecs/tools/statistics/data_transformer_tool.py +580 -0
- aiecs/tools/statistics/data_visualizer_tool.py +498 -0
- aiecs/tools/statistics/model_trainer_tool.py +507 -0
- aiecs/tools/statistics/statistical_analyzer_tool.py +472 -0
- aiecs/tools/task_tools/__init__.py +49 -36
- aiecs/tools/task_tools/chart_tool.py +200 -184
- aiecs/tools/task_tools/classfire_tool.py +268 -267
- aiecs/tools/task_tools/image_tool.py +220 -141
- aiecs/tools/task_tools/office_tool.py +226 -146
- aiecs/tools/task_tools/pandas_tool.py +477 -121
- aiecs/tools/task_tools/report_tool.py +390 -142
- aiecs/tools/task_tools/research_tool.py +149 -79
- aiecs/tools/task_tools/scraper_tool.py +339 -145
- aiecs/tools/task_tools/stats_tool.py +448 -209
- aiecs/tools/temp_file_manager.py +26 -24
- aiecs/tools/tool_executor/__init__.py +18 -16
- aiecs/tools/tool_executor/tool_executor.py +364 -52
- aiecs/utils/LLM_output_structor.py +74 -48
- aiecs/utils/__init__.py +14 -3
- aiecs/utils/base_callback.py +0 -3
- aiecs/utils/cache_provider.py +696 -0
- aiecs/utils/execution_utils.py +50 -31
- aiecs/utils/prompt_loader.py +1 -0
- aiecs/utils/token_usage_repository.py +37 -11
- aiecs/ws/socket_server.py +14 -4
- {aiecs-1.0.1.dist-info → aiecs-1.7.17.dist-info}/METADATA +52 -15
- aiecs-1.7.17.dist-info/RECORD +337 -0
- aiecs-1.7.17.dist-info/entry_points.txt +13 -0
- aiecs/config/registry.py +0 -19
- aiecs/domain/context/content_engine.py +0 -982
- aiecs/llm/base_client.py +0 -99
- aiecs/llm/openai_client.py +0 -125
- aiecs/llm/vertex_client.py +0 -186
- aiecs/llm/xai_client.py +0 -184
- aiecs/scripts/dependency_checker.py +0 -857
- aiecs/scripts/quick_dependency_check.py +0 -269
- aiecs/tools/task_tools/search_api.py +0 -7
- aiecs-1.0.1.dist-info/RECORD +0 -90
- aiecs-1.0.1.dist-info/entry_points.txt +0 -7
- /aiecs/scripts/{setup_nlp_data.sh → dependance_check/setup_nlp_data.sh} +0 -0
- /aiecs/scripts/{README_WEASEL_PATCH.md → dependance_patch/fix_weasel/README_WEASEL_PATCH.md} +0 -0
- /aiecs/scripts/{fix_weasel_validator.sh → dependance_patch/fix_weasel/fix_weasel_validator.sh} +0 -0
- /aiecs/scripts/{run_weasel_patch.sh → dependance_patch/fix_weasel/run_weasel_patch.sh} +0 -0
- {aiecs-1.0.1.dist-info → aiecs-1.7.17.dist-info}/WHEEL +0 -0
- {aiecs-1.0.1.dist-info → aiecs-1.7.17.dist-info}/licenses/LICENSE +0 -0
- {aiecs-1.0.1.dist-info → aiecs-1.7.17.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,3949 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Base AI Agent
|
|
3
|
+
|
|
4
|
+
Abstract base class for all AI agents in the AIECS system.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from datetime import datetime
|
|
9
|
+
from typing import Dict, List, Any, Optional, Callable, Union, TYPE_CHECKING, AsyncIterator, Set
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
import logging
|
|
12
|
+
import time
|
|
13
|
+
import asyncio
|
|
14
|
+
import json
|
|
15
|
+
|
|
16
|
+
from .models import (
|
|
17
|
+
AgentState,
|
|
18
|
+
AgentType,
|
|
19
|
+
AgentConfiguration,
|
|
20
|
+
AgentGoal,
|
|
21
|
+
AgentMetrics,
|
|
22
|
+
AgentCapabilityDeclaration,
|
|
23
|
+
GoalStatus,
|
|
24
|
+
GoalPriority,
|
|
25
|
+
MemoryType,
|
|
26
|
+
)
|
|
27
|
+
from .exceptions import (
|
|
28
|
+
InvalidStateTransitionError,
|
|
29
|
+
ConfigurationError,
|
|
30
|
+
AgentInitializationError,
|
|
31
|
+
SerializationError,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
# Import protocols for type hints
|
|
35
|
+
if TYPE_CHECKING:
|
|
36
|
+
from aiecs.llm.protocols import LLMClientProtocol
|
|
37
|
+
from aiecs.domain.agent.integration.protocols import (
|
|
38
|
+
ConfigManagerProtocol,
|
|
39
|
+
CheckpointerProtocol,
|
|
40
|
+
)
|
|
41
|
+
from aiecs.tools.base_tool import BaseTool
|
|
42
|
+
from aiecs.domain.context.context_engine import ContextEngine
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class OperationTimer:
|
|
48
|
+
"""
|
|
49
|
+
Context manager for timing operations and tracking metrics.
|
|
50
|
+
|
|
51
|
+
Automatically records operation duration and can be used to track
|
|
52
|
+
operation-level performance metrics.
|
|
53
|
+
|
|
54
|
+
Example:
|
|
55
|
+
with agent.track_operation_time("llm_call") as timer:
|
|
56
|
+
result = llm.generate(prompt)
|
|
57
|
+
# timer.duration contains the elapsed time in seconds
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
def __init__(self, operation_name: str, agent: Optional["BaseAIAgent"] = None):
|
|
61
|
+
"""
|
|
62
|
+
Initialize operation timer.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
operation_name: Name of the operation being timed
|
|
66
|
+
agent: Optional agent instance for automatic metrics recording
|
|
67
|
+
"""
|
|
68
|
+
self.operation_name = operation_name
|
|
69
|
+
self.agent = agent
|
|
70
|
+
self.start_time: Optional[float] = None
|
|
71
|
+
self.end_time: Optional[float] = None
|
|
72
|
+
self.duration: Optional[float] = None
|
|
73
|
+
self.error: Optional[Exception] = None
|
|
74
|
+
|
|
75
|
+
def __enter__(self) -> "OperationTimer":
|
|
76
|
+
"""Start timing."""
|
|
77
|
+
self.start_time = time.time()
|
|
78
|
+
return self
|
|
79
|
+
|
|
80
|
+
def __exit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
81
|
+
"""
|
|
82
|
+
Stop timing and record metrics.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
exc_type: Exception type if an error occurred
|
|
86
|
+
exc_val: Exception value if an error occurred
|
|
87
|
+
exc_tb: Exception traceback if an error occurred
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
False to propagate exceptions
|
|
91
|
+
"""
|
|
92
|
+
self.end_time = time.time()
|
|
93
|
+
if self.start_time is not None:
|
|
94
|
+
self.duration = self.end_time - self.start_time
|
|
95
|
+
|
|
96
|
+
# Track error if one occurred
|
|
97
|
+
if exc_val is not None:
|
|
98
|
+
self.error = exc_val
|
|
99
|
+
|
|
100
|
+
# Record metrics if agent is provided
|
|
101
|
+
if self.agent and self.duration is not None:
|
|
102
|
+
self.agent._record_operation_metrics(
|
|
103
|
+
operation_name=self.operation_name,
|
|
104
|
+
duration=self.duration,
|
|
105
|
+
success=exc_val is None,
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Don't suppress exceptions
|
|
109
|
+
return None
|
|
110
|
+
|
|
111
|
+
def get_duration_ms(self) -> Optional[float]:
|
|
112
|
+
"""Get duration in milliseconds."""
|
|
113
|
+
return self.duration * 1000 if self.duration is not None else None
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@dataclass
|
|
117
|
+
class CacheConfig:
|
|
118
|
+
"""
|
|
119
|
+
Configuration for tool result caching.
|
|
120
|
+
|
|
121
|
+
Provides control over caching behavior to improve performance and reduce costs
|
|
122
|
+
by avoiding redundant tool executions. Supports TTL-based expiration, size limits,
|
|
123
|
+
and automatic cleanup.
|
|
124
|
+
|
|
125
|
+
**Key Features:**
|
|
126
|
+
- TTL-based cache expiration (default and per-tool)
|
|
127
|
+
- Size limits to prevent memory exhaustion
|
|
128
|
+
- Automatic cleanup when capacity threshold reached
|
|
129
|
+
- Configurable cache key generation
|
|
130
|
+
- Input hashing for large parameters
|
|
131
|
+
|
|
132
|
+
Attributes:
|
|
133
|
+
enabled: Enable/disable caching globally
|
|
134
|
+
default_ttl: Default time-to-live in seconds for cached entries (default: 300 = 5 minutes)
|
|
135
|
+
tool_specific_ttl: Dictionary mapping tool names to custom TTL values (overrides default_ttl)
|
|
136
|
+
max_cache_size: Maximum number of cached entries before cleanup (default: 1000)
|
|
137
|
+
max_memory_mb: Maximum cache memory usage in MB (approximate, default: 100)
|
|
138
|
+
cleanup_interval: Interval in seconds between cleanup checks (default: 60)
|
|
139
|
+
cleanup_threshold: Capacity threshold (0.0-1.0) to trigger cleanup (default: 0.9 = 90%)
|
|
140
|
+
include_timestamp_in_key: Whether to include timestamp in cache key (default: False)
|
|
141
|
+
hash_large_inputs: Whether to hash inputs larger than 1KB for cache keys (default: True)
|
|
142
|
+
|
|
143
|
+
Examples:
|
|
144
|
+
# Example 1: Basic caching configuration
|
|
145
|
+
config = CacheConfig(
|
|
146
|
+
enabled=True,
|
|
147
|
+
default_ttl=300, # 5 minutes
|
|
148
|
+
max_cache_size=1000
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
# Example 2: Per-tool TTL overrides
|
|
152
|
+
config = CacheConfig(
|
|
153
|
+
enabled=True,
|
|
154
|
+
default_ttl=300,
|
|
155
|
+
tool_specific_ttl={
|
|
156
|
+
"search": 600, # Search results cached for 10 minutes
|
|
157
|
+
"calculator": 3600, # Calculator results cached for 1 hour
|
|
158
|
+
"weather": 1800 # Weather data cached for 30 minutes
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
# Example 3: Aggressive caching for expensive operations
|
|
163
|
+
config = CacheConfig(
|
|
164
|
+
enabled=True,
|
|
165
|
+
default_ttl=3600, # 1 hour default
|
|
166
|
+
max_cache_size=5000,
|
|
167
|
+
max_memory_mb=500,
|
|
168
|
+
cleanup_threshold=0.95 # Cleanup at 95% capacity
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# Example 4: Disable caching for time-sensitive tools
|
|
172
|
+
config = CacheConfig(
|
|
173
|
+
enabled=False # Disable caching entirely
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
# Example 5: Cache with timestamp-aware keys
|
|
177
|
+
config = CacheConfig(
|
|
178
|
+
enabled=True,
|
|
179
|
+
default_ttl=300,
|
|
180
|
+
include_timestamp_in_key=True # Include timestamp for time-sensitive caching
|
|
181
|
+
)
|
|
182
|
+
"""
|
|
183
|
+
|
|
184
|
+
# Cache enablement
|
|
185
|
+
enabled: bool = True # Enable/disable caching
|
|
186
|
+
|
|
187
|
+
# TTL settings
|
|
188
|
+
default_ttl: int = 300 # Default TTL in seconds (5 minutes)
|
|
189
|
+
tool_specific_ttl: Optional[Dict[str, int]] = None # Per-tool TTL overrides
|
|
190
|
+
|
|
191
|
+
# Size limits
|
|
192
|
+
max_cache_size: int = 1000 # Maximum number of cached entries
|
|
193
|
+
max_memory_mb: int = 100 # Maximum cache memory in MB (approximate)
|
|
194
|
+
|
|
195
|
+
# Cleanup settings
|
|
196
|
+
cleanup_interval: int = 60 # Cleanup interval in seconds
|
|
197
|
+
cleanup_threshold: float = 0.9 # Trigger cleanup at 90% capacity
|
|
198
|
+
|
|
199
|
+
# Cache key settings
|
|
200
|
+
include_timestamp_in_key: bool = False # Include timestamp in cache key
|
|
201
|
+
hash_large_inputs: bool = True # Hash inputs larger than 1KB
|
|
202
|
+
|
|
203
|
+
def __post_init__(self):
|
|
204
|
+
"""Initialize defaults."""
|
|
205
|
+
if self.tool_specific_ttl is None:
|
|
206
|
+
self.tool_specific_ttl = {}
|
|
207
|
+
|
|
208
|
+
def get_ttl(self, tool_name: str) -> int:
|
|
209
|
+
"""
|
|
210
|
+
Get TTL for a specific tool.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
tool_name: Name of the tool
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
TTL in seconds
|
|
217
|
+
"""
|
|
218
|
+
if self.tool_specific_ttl is None:
|
|
219
|
+
return self.default_ttl
|
|
220
|
+
return self.tool_specific_ttl.get(tool_name, self.default_ttl)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class BaseAIAgent(ABC):
|
|
224
|
+
"""
|
|
225
|
+
Abstract base class for AI agents.
|
|
226
|
+
|
|
227
|
+
Provides common functionality for agent lifecycle management,
|
|
228
|
+
state management, memory, goals, and metrics tracking.
|
|
229
|
+
|
|
230
|
+
This base class supports extensive flexibility and advanced features:
|
|
231
|
+
|
|
232
|
+
**Tool Flexibility:**
|
|
233
|
+
- Accept tool names (List[str]) for backward compatibility
|
|
234
|
+
- Accept pre-configured tool instances (Dict[str, BaseTool]) with preserved state
|
|
235
|
+
- Automatic tool loading and validation
|
|
236
|
+
|
|
237
|
+
**LLM Client Flexibility:**
|
|
238
|
+
- Accept any object implementing LLMClientProtocol (duck typing)
|
|
239
|
+
- No requirement for BaseLLMClient inheritance
|
|
240
|
+
- Custom LLM client wrappers fully supported
|
|
241
|
+
|
|
242
|
+
**Advanced Features:**
|
|
243
|
+
- ContextEngine integration for persistent conversation history
|
|
244
|
+
- Custom config managers for dynamic configuration
|
|
245
|
+
- Checkpointers for state persistence (LangGraph compatible)
|
|
246
|
+
- Agent collaboration (delegation, peer review, consensus)
|
|
247
|
+
- Agent learning from experiences
|
|
248
|
+
- Resource management (rate limiting, quotas)
|
|
249
|
+
- Performance tracking and health monitoring
|
|
250
|
+
- Tool result caching
|
|
251
|
+
- Parallel tool execution
|
|
252
|
+
- Streaming responses
|
|
253
|
+
- Error recovery strategies
|
|
254
|
+
|
|
255
|
+
Examples:
|
|
256
|
+
# Example 1: Basic agent with tool names (backward compatible)
|
|
257
|
+
agent = HybridAgent(
|
|
258
|
+
agent_id="agent1",
|
|
259
|
+
name="My Agent",
|
|
260
|
+
agent_type=AgentType.HYBRID,
|
|
261
|
+
config=config,
|
|
262
|
+
tools=["search", "calculator"] # Tool names loaded by subclass
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
# Example 2: Agent with tool instances (preserves tool state)
|
|
266
|
+
from aiecs.tools import BaseTool
|
|
267
|
+
|
|
268
|
+
class StatefulSearchTool(BaseTool):
|
|
269
|
+
def __init__(self, api_key: str):
|
|
270
|
+
self.api_key = api_key
|
|
271
|
+
self.call_count = 0 # State preserved
|
|
272
|
+
|
|
273
|
+
async def run_async(self, query: str):
|
|
274
|
+
self.call_count += 1
|
|
275
|
+
return f"Search results for: {query}"
|
|
276
|
+
|
|
277
|
+
agent = HybridAgent(
|
|
278
|
+
agent_id="agent1",
|
|
279
|
+
name="My Agent",
|
|
280
|
+
agent_type=AgentType.HYBRID,
|
|
281
|
+
config=config,
|
|
282
|
+
tools={
|
|
283
|
+
"search": StatefulSearchTool(api_key="..."),
|
|
284
|
+
"calculator": CalculatorTool()
|
|
285
|
+
},
|
|
286
|
+
llm_client=OpenAIClient()
|
|
287
|
+
)
|
|
288
|
+
# Tool state (call_count) is preserved across agent operations
|
|
289
|
+
|
|
290
|
+
# Example 3: Agent with custom LLM client (no BaseLLMClient inheritance)
|
|
291
|
+
class CustomLLMClient:
|
|
292
|
+
provider_name = "custom"
|
|
293
|
+
|
|
294
|
+
async def generate_text(self, messages, **kwargs):
|
|
295
|
+
# Custom implementation
|
|
296
|
+
return LLMResponse(content="...", provider="custom")
|
|
297
|
+
|
|
298
|
+
async def stream_text(self, messages, **kwargs):
|
|
299
|
+
async for token in self._custom_stream():
|
|
300
|
+
yield token
|
|
301
|
+
|
|
302
|
+
async def close(self):
|
|
303
|
+
# Cleanup
|
|
304
|
+
pass
|
|
305
|
+
|
|
306
|
+
agent = LLMAgent(
|
|
307
|
+
agent_id="agent1",
|
|
308
|
+
name="My LLM Agent",
|
|
309
|
+
agent_type=AgentType.CONVERSATIONAL,
|
|
310
|
+
config=config,
|
|
311
|
+
llm_client=CustomLLMClient() # Works without BaseLLMClient!
|
|
312
|
+
)
|
|
313
|
+
|
|
314
|
+
# Example 4: Agent with ContextEngine for persistent storage
|
|
315
|
+
from aiecs.domain.context import ContextEngine
|
|
316
|
+
|
|
317
|
+
context_engine = ContextEngine()
|
|
318
|
+
await context_engine.initialize()
|
|
319
|
+
|
|
320
|
+
agent = HybridAgent(
|
|
321
|
+
agent_id="agent1",
|
|
322
|
+
name="My Agent",
|
|
323
|
+
agent_type=AgentType.HYBRID,
|
|
324
|
+
config=config,
|
|
325
|
+
tools=["search"],
|
|
326
|
+
llm_client=OpenAIClient(),
|
|
327
|
+
context_engine=context_engine # Enables persistent conversation history
|
|
328
|
+
)
|
|
329
|
+
# Conversation history persists across agent restarts
|
|
330
|
+
|
|
331
|
+
# Example 5: Agent with custom config manager
|
|
332
|
+
class DatabaseConfigManager:
|
|
333
|
+
async def get_config(self, key: str):
|
|
334
|
+
# Load from database
|
|
335
|
+
return await db.get_config(key)
|
|
336
|
+
|
|
337
|
+
async def update_config(self, key: str, value: Any):
|
|
338
|
+
# Update in database
|
|
339
|
+
await db.update_config(key, value)
|
|
340
|
+
|
|
341
|
+
agent = HybridAgent(
|
|
342
|
+
agent_id="agent1",
|
|
343
|
+
name="My Agent",
|
|
344
|
+
agent_type=AgentType.HYBRID,
|
|
345
|
+
config=config,
|
|
346
|
+
tools=["search"],
|
|
347
|
+
llm_client=OpenAIClient(),
|
|
348
|
+
config_manager=DatabaseConfigManager() # Dynamic config loading
|
|
349
|
+
)
|
|
350
|
+
|
|
351
|
+
# Example 6: Agent with checkpointer for LangGraph integration
|
|
352
|
+
class RedisCheckpointer:
|
|
353
|
+
async def save(self, agent_id: str, state: Dict[str, Any]):
|
|
354
|
+
await redis.set(f"checkpoint:{agent_id}", json.dumps(state))
|
|
355
|
+
|
|
356
|
+
async def load(self, agent_id: str) -> Optional[Dict[str, Any]]:
|
|
357
|
+
data = await redis.get(f"checkpoint:{agent_id}")
|
|
358
|
+
return json.loads(data) if data else None
|
|
359
|
+
|
|
360
|
+
agent = HybridAgent(
|
|
361
|
+
agent_id="agent1",
|
|
362
|
+
name="My Agent",
|
|
363
|
+
agent_type=AgentType.HYBRID,
|
|
364
|
+
config=config,
|
|
365
|
+
tools=["search"],
|
|
366
|
+
llm_client=OpenAIClient(),
|
|
367
|
+
checkpointer=RedisCheckpointer() # LangGraph-compatible checkpointing
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
# Example 7: Agent with collaboration features
|
|
371
|
+
agent_registry = {
|
|
372
|
+
"agent2": other_agent_instance,
|
|
373
|
+
"agent3": another_agent_instance
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
agent = HybridAgent(
|
|
377
|
+
agent_id="agent1",
|
|
378
|
+
name="My Agent",
|
|
379
|
+
agent_type=AgentType.HYBRID,
|
|
380
|
+
config=config,
|
|
381
|
+
tools=["search"],
|
|
382
|
+
llm_client=OpenAIClient(),
|
|
383
|
+
collaboration_enabled=True,
|
|
384
|
+
agent_registry=agent_registry # Enable delegation and peer review
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# Delegate task to another agent
|
|
388
|
+
result = await agent.delegate_task(
|
|
389
|
+
task_description="Analyze this data",
|
|
390
|
+
target_agent_id="agent2"
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
# Example 8: Agent with learning enabled
|
|
394
|
+
from aiecs.domain.agent.models import ResourceLimits
|
|
395
|
+
|
|
396
|
+
agent = HybridAgent(
|
|
397
|
+
agent_id="agent1",
|
|
398
|
+
name="My Agent",
|
|
399
|
+
agent_type=AgentType.HYBRID,
|
|
400
|
+
config=config,
|
|
401
|
+
tools=["search"],
|
|
402
|
+
llm_client=OpenAIClient(),
|
|
403
|
+
learning_enabled=True # Learn from past experiences
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
# Record experience
|
|
407
|
+
await agent.record_experience(
|
|
408
|
+
task_type="data_analysis",
|
|
409
|
+
approach="parallel_tools",
|
|
410
|
+
success=True,
|
|
411
|
+
execution_time=2.5
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
# Get recommended approach based on history
|
|
415
|
+
approach = await agent.get_recommended_approach("data_analysis")
|
|
416
|
+
print(f"Recommended: {approach}")
|
|
417
|
+
|
|
418
|
+
# Example 9: Agent with resource limits
|
|
419
|
+
from aiecs.domain.agent.models import ResourceLimits
|
|
420
|
+
|
|
421
|
+
resource_limits = ResourceLimits(
|
|
422
|
+
max_concurrent_tasks=5,
|
|
423
|
+
max_tokens_per_minute=10000,
|
|
424
|
+
max_tool_calls_per_minute=100
|
|
425
|
+
)
|
|
426
|
+
|
|
427
|
+
agent = HybridAgent(
|
|
428
|
+
agent_id="agent1",
|
|
429
|
+
name="My Agent",
|
|
430
|
+
agent_type=AgentType.HYBRID,
|
|
431
|
+
config=config,
|
|
432
|
+
tools=["search"],
|
|
433
|
+
llm_client=OpenAIClient(),
|
|
434
|
+
resource_limits=resource_limits # Rate limiting and quotas
|
|
435
|
+
)
|
|
436
|
+
|
|
437
|
+
# Check resource availability before executing
|
|
438
|
+
if await agent.check_resource_availability():
|
|
439
|
+
result = await agent.execute_task(task, context)
|
|
440
|
+
else:
|
|
441
|
+
await agent.wait_for_resources(timeout=30.0)
|
|
442
|
+
|
|
443
|
+
# Example 10: Agent with performance tracking
|
|
444
|
+
agent = HybridAgent(
|
|
445
|
+
agent_id="agent1",
|
|
446
|
+
name="My Agent",
|
|
447
|
+
agent_type=AgentType.HYBRID,
|
|
448
|
+
config=config,
|
|
449
|
+
tools=["search"],
|
|
450
|
+
llm_client=OpenAIClient()
|
|
451
|
+
)
|
|
452
|
+
|
|
453
|
+
# Track operation performance
|
|
454
|
+
with agent.track_operation_time("data_processing"):
|
|
455
|
+
result = await agent.execute_task(task, context)
|
|
456
|
+
|
|
457
|
+
# Get performance metrics
|
|
458
|
+
metrics = agent.get_performance_metrics()
|
|
459
|
+
print(f"Average response time: {metrics['avg_response_time']}s")
|
|
460
|
+
print(f"P95 response time: {metrics['p95_response_time']}s")
|
|
461
|
+
|
|
462
|
+
# Get health status
|
|
463
|
+
health = agent.get_health_status()
|
|
464
|
+
print(f"Health score: {health['score']}")
|
|
465
|
+
print(f"Status: {health['status']}")
|
|
466
|
+
|
|
467
|
+
# Example 11: Agent with tool caching
|
|
468
|
+
agent = HybridAgent(
|
|
469
|
+
agent_id="agent1",
|
|
470
|
+
name="My Agent",
|
|
471
|
+
agent_type=AgentType.HYBRID,
|
|
472
|
+
config=config,
|
|
473
|
+
tools=["search"],
|
|
474
|
+
llm_client=OpenAIClient()
|
|
475
|
+
)
|
|
476
|
+
|
|
477
|
+
# Execute tool with caching (30 second TTL)
|
|
478
|
+
result1 = await agent.execute_tool_with_cache(
|
|
479
|
+
tool_name="search",
|
|
480
|
+
operation="query",
|
|
481
|
+
parameters={"q": "AI"},
|
|
482
|
+
cache_ttl=30
|
|
483
|
+
)
|
|
484
|
+
|
|
485
|
+
# Second call uses cache (no API call)
|
|
486
|
+
result2 = await agent.execute_tool_with_cache(
|
|
487
|
+
tool_name="search",
|
|
488
|
+
operation="query",
|
|
489
|
+
parameters={"q": "AI"},
|
|
490
|
+
cache_ttl=30
|
|
491
|
+
)
|
|
492
|
+
|
|
493
|
+
# Get cache statistics
|
|
494
|
+
stats = agent.get_cache_stats()
|
|
495
|
+
print(f"Cache hit rate: {stats['hit_rate']:.1%}")
|
|
496
|
+
|
|
497
|
+
# Example 12: Agent with parallel tool execution
|
|
498
|
+
agent = HybridAgent(
|
|
499
|
+
agent_id="agent1",
|
|
500
|
+
name="My Agent",
|
|
501
|
+
agent_type=AgentType.HYBRID,
|
|
502
|
+
config=config,
|
|
503
|
+
tools=["search", "calculator", "translator"],
|
|
504
|
+
llm_client=OpenAIClient()
|
|
505
|
+
)
|
|
506
|
+
|
|
507
|
+
# Execute multiple independent tools in parallel (3-5x faster)
|
|
508
|
+
results = await agent.execute_tools_parallel([
|
|
509
|
+
{"tool": "search", "operation": "query", "parameters": {"q": "AI"}},
|
|
510
|
+
{"tool": "calculator", "operation": "add", "parameters": {"a": 1, "b": 2}},
|
|
511
|
+
{"tool": "translator", "operation": "translate", "parameters": {"text": "Hello"}}
|
|
512
|
+
], max_concurrency=3)
|
|
513
|
+
|
|
514
|
+
# Example 13: Agent with streaming responses
|
|
515
|
+
agent = HybridAgent(
|
|
516
|
+
agent_id="agent1",
|
|
517
|
+
name="My Agent",
|
|
518
|
+
agent_type=AgentType.HYBRID,
|
|
519
|
+
config=config,
|
|
520
|
+
tools=["search"],
|
|
521
|
+
llm_client=OpenAIClient()
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
# Stream task execution (tokens + tool calls)
|
|
525
|
+
async for event in agent.execute_task_streaming(task, context):
|
|
526
|
+
if event['type'] == 'token':
|
|
527
|
+
print(event['content'], end='', flush=True)
|
|
528
|
+
elif event['type'] == 'tool_call':
|
|
529
|
+
print(f"\\nCalling {event['tool_name']}...")
|
|
530
|
+
elif event['type'] == 'result':
|
|
531
|
+
print(f"\\nFinal result: {event['output']}")
|
|
532
|
+
|
|
533
|
+
# Example 14: Agent with error recovery
|
|
534
|
+
agent = HybridAgent(
|
|
535
|
+
agent_id="agent1",
|
|
536
|
+
name="My Agent",
|
|
537
|
+
agent_type=AgentType.HYBRID,
|
|
538
|
+
config=config,
|
|
539
|
+
tools=["search"],
|
|
540
|
+
llm_client=OpenAIClient()
|
|
541
|
+
)
|
|
542
|
+
|
|
543
|
+
# Execute with automatic recovery strategies
|
|
544
|
+
result = await agent.execute_with_recovery(
|
|
545
|
+
task=task,
|
|
546
|
+
context=context,
|
|
547
|
+
strategies=["retry", "simplify", "fallback", "delegate"]
|
|
548
|
+
)
|
|
549
|
+
# Automatically tries retry → simplify → fallback → delegate if errors occur
|
|
550
|
+
"""
|
|
551
|
+
|
|
552
|
+
def __init__(
|
|
553
|
+
self,
|
|
554
|
+
agent_id: str,
|
|
555
|
+
name: str,
|
|
556
|
+
agent_type: AgentType,
|
|
557
|
+
config: AgentConfiguration,
|
|
558
|
+
description: Optional[str] = None,
|
|
559
|
+
version: str = "1.0.0",
|
|
560
|
+
tools: Optional[Union[List[str], Dict[str, "BaseTool"]]] = None,
|
|
561
|
+
llm_client: Optional["LLMClientProtocol"] = None,
|
|
562
|
+
config_manager: Optional["ConfigManagerProtocol"] = None,
|
|
563
|
+
checkpointer: Optional["CheckpointerProtocol"] = None,
|
|
564
|
+
context_engine: Optional["ContextEngine"] = None,
|
|
565
|
+
collaboration_enabled: bool = False,
|
|
566
|
+
agent_registry: Optional[Dict[str, Any]] = None,
|
|
567
|
+
learning_enabled: bool = False,
|
|
568
|
+
resource_limits: Optional[Any] = None,
|
|
569
|
+
):
|
|
570
|
+
"""
|
|
571
|
+
Initialize the base agent.
|
|
572
|
+
|
|
573
|
+
Args:
|
|
574
|
+
agent_id: Unique identifier for the agent
|
|
575
|
+
name: Agent name
|
|
576
|
+
agent_type: Type of agent
|
|
577
|
+
config: Agent configuration
|
|
578
|
+
description: Optional agent description
|
|
579
|
+
version: Agent version
|
|
580
|
+
tools: Optional tools - either list of tool names or dict of tool instances.
|
|
581
|
+
List[str]: Tool names to be loaded by subclass
|
|
582
|
+
Dict[str, BaseTool]: Pre-configured tool instances with state
|
|
583
|
+
llm_client: Optional LLM client (any object implementing LLMClientProtocol).
|
|
584
|
+
Supports custom LLM clients without BaseLLMClient inheritance.
|
|
585
|
+
config_manager: Optional configuration manager for dynamic config loading
|
|
586
|
+
checkpointer: Optional checkpointer for state persistence (LangGraph compatible)
|
|
587
|
+
context_engine: Optional ContextEngine instance for persistent conversation history
|
|
588
|
+
collaboration_enabled: Enable agent collaboration features (delegation, peer review)
|
|
589
|
+
agent_registry: Registry of other agents for collaboration (agent_id -> agent instance)
|
|
590
|
+
learning_enabled: Enable agent learning from experiences
|
|
591
|
+
resource_limits: Optional resource limits configuration
|
|
592
|
+
and session management. If provided, enables persistent storage
|
|
593
|
+
across agent restarts.
|
|
594
|
+
|
|
595
|
+
Example:
|
|
596
|
+
# With tool instances and ContextEngine
|
|
597
|
+
from aiecs.domain.context import ContextEngine
|
|
598
|
+
|
|
599
|
+
context_engine = ContextEngine()
|
|
600
|
+
await context_engine.initialize()
|
|
601
|
+
|
|
602
|
+
agent = HybridAgent(
|
|
603
|
+
agent_id="agent1",
|
|
604
|
+
name="My Agent",
|
|
605
|
+
agent_type=AgentType.HYBRID,
|
|
606
|
+
config=config,
|
|
607
|
+
tools={
|
|
608
|
+
"search": SearchTool(api_key="..."),
|
|
609
|
+
"calculator": CalculatorTool()
|
|
610
|
+
},
|
|
611
|
+
llm_client=CustomLLMClient(), # Custom client, no inheritance needed
|
|
612
|
+
config_manager=DatabaseConfigManager(),
|
|
613
|
+
checkpointer=RedisCheckpointer(),
|
|
614
|
+
context_engine=context_engine # Enables persistent storage
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
# With tool names (backward compatible)
|
|
618
|
+
agent = HybridAgent(
|
|
619
|
+
agent_id="agent1",
|
|
620
|
+
name="My Agent",
|
|
621
|
+
agent_type=AgentType.HYBRID,
|
|
622
|
+
config=config,
|
|
623
|
+
tools=["search", "calculator"] # Loaded by subclass
|
|
624
|
+
)
|
|
625
|
+
"""
|
|
626
|
+
# Identity
|
|
627
|
+
self.agent_id = agent_id
|
|
628
|
+
self.name = name
|
|
629
|
+
self.agent_type = agent_type
|
|
630
|
+
self.description = description or f"{agent_type.value} agent"
|
|
631
|
+
self.version = version
|
|
632
|
+
|
|
633
|
+
# Configuration
|
|
634
|
+
self._config = config
|
|
635
|
+
self._config_manager = config_manager
|
|
636
|
+
|
|
637
|
+
# State
|
|
638
|
+
self._state = AgentState.CREATED
|
|
639
|
+
self._previous_state: Optional[AgentState] = None
|
|
640
|
+
|
|
641
|
+
# Memory storage (in-memory dict, can be replaced with sophisticated
|
|
642
|
+
# storage)
|
|
643
|
+
self._memory: Dict[str, Any] = {}
|
|
644
|
+
self._memory_metadata: Dict[str, Dict[str, Any]] = {}
|
|
645
|
+
|
|
646
|
+
# Goals
|
|
647
|
+
self._goals: Dict[str, AgentGoal] = {}
|
|
648
|
+
|
|
649
|
+
# Capabilities
|
|
650
|
+
self._capabilities: Dict[str, AgentCapabilityDeclaration] = {}
|
|
651
|
+
|
|
652
|
+
# Metrics
|
|
653
|
+
self._metrics = AgentMetrics() # type: ignore[call-arg]
|
|
654
|
+
|
|
655
|
+
# Timestamps
|
|
656
|
+
self.created_at = datetime.utcnow()
|
|
657
|
+
self.updated_at = datetime.utcnow()
|
|
658
|
+
self.last_active_at: Optional[datetime] = None
|
|
659
|
+
|
|
660
|
+
# Current task tracking
|
|
661
|
+
self._current_task_id: Optional[str] = None
|
|
662
|
+
|
|
663
|
+
# Tools (optional - only set if tools provided)
|
|
664
|
+
self._tools_input = tools # Store original input
|
|
665
|
+
self._available_tools: Optional[List[str]] = None
|
|
666
|
+
self._tool_instances: Optional[Dict[str, "BaseTool"]] = None
|
|
667
|
+
|
|
668
|
+
# LLM client (optional)
|
|
669
|
+
self._llm_client = llm_client
|
|
670
|
+
|
|
671
|
+
# Checkpointer (optional)
|
|
672
|
+
self._checkpointer = checkpointer
|
|
673
|
+
|
|
674
|
+
# ContextEngine (optional - Phase 4 enhancement)
|
|
675
|
+
self._context_engine = context_engine
|
|
676
|
+
|
|
677
|
+
# Tool result cache (Phase 7 enhancement)
|
|
678
|
+
self._cache_config = CacheConfig()
|
|
679
|
+
self._tool_cache: Dict[str, Any] = {} # Cache key -> result
|
|
680
|
+
self._cache_timestamps: Dict[str, float] = {} # Cache key -> timestamp
|
|
681
|
+
self._cache_access_count: Dict[str, int] = {} # Cache key -> access count
|
|
682
|
+
self._last_cleanup_time = time.time()
|
|
683
|
+
|
|
684
|
+
# Agent collaboration (Phase 7 enhancement - tasks 1.15.15-1.15.22)
|
|
685
|
+
self._collaboration_enabled = collaboration_enabled
|
|
686
|
+
self._agent_registry = agent_registry or {}
|
|
687
|
+
|
|
688
|
+
# Agent learning (Phase 8 enhancement - tasks 1.16.4-1.16.10)
|
|
689
|
+
self._learning_enabled = learning_enabled
|
|
690
|
+
self._experiences: List[Any] = [] # List of Experience objects
|
|
691
|
+
self._max_experiences = 1000 # Limit stored experiences
|
|
692
|
+
|
|
693
|
+
# Resource management (Phase 8 enhancement - tasks 1.16.11-1.16.17)
|
|
694
|
+
from .models import ResourceLimits
|
|
695
|
+
|
|
696
|
+
self._resource_limits = resource_limits or ResourceLimits() # type: ignore[call-arg]
|
|
697
|
+
self._active_tasks: set = set() # Set of active task IDs
|
|
698
|
+
self._token_usage_window: List[tuple] = [] # List of (timestamp, token_count)
|
|
699
|
+
self._tool_call_window: List[float] = [] # List of timestamps
|
|
700
|
+
|
|
701
|
+
features = []
|
|
702
|
+
if context_engine:
|
|
703
|
+
features.append("ContextEngine")
|
|
704
|
+
if collaboration_enabled:
|
|
705
|
+
features.append("collaboration")
|
|
706
|
+
if learning_enabled:
|
|
707
|
+
features.append("learning")
|
|
708
|
+
if resource_limits:
|
|
709
|
+
features.append("resource limits")
|
|
710
|
+
|
|
711
|
+
feature_str = f" with {', '.join(features)}" if features else ""
|
|
712
|
+
logger.info(f"Agent initialized: {self.agent_id} ({self.name}, {self.agent_type.value}){feature_str}")
|
|
713
|
+
|
|
714
|
+
# ==================== State Management ====================
|
|
715
|
+
|
|
716
|
+
@property
|
|
717
|
+
def state(self) -> AgentState:
|
|
718
|
+
"""Get current agent state."""
|
|
719
|
+
return self._state
|
|
720
|
+
|
|
721
|
+
def get_state(self) -> AgentState:
|
|
722
|
+
"""Get current agent state."""
|
|
723
|
+
return self._state
|
|
724
|
+
|
|
725
|
+
def _transition_state(self, new_state: AgentState) -> None:
|
|
726
|
+
"""
|
|
727
|
+
Transition to a new state with validation.
|
|
728
|
+
|
|
729
|
+
Args:
|
|
730
|
+
new_state: Target state
|
|
731
|
+
|
|
732
|
+
Raises:
|
|
733
|
+
InvalidStateTransitionError: If transition is invalid
|
|
734
|
+
"""
|
|
735
|
+
# Define valid transitions
|
|
736
|
+
valid_transitions = {
|
|
737
|
+
AgentState.CREATED: {AgentState.INITIALIZING},
|
|
738
|
+
AgentState.INITIALIZING: {AgentState.ACTIVE, AgentState.ERROR},
|
|
739
|
+
AgentState.ACTIVE: {
|
|
740
|
+
AgentState.BUSY,
|
|
741
|
+
AgentState.IDLE,
|
|
742
|
+
AgentState.STOPPED,
|
|
743
|
+
AgentState.ERROR,
|
|
744
|
+
},
|
|
745
|
+
AgentState.BUSY: {AgentState.ACTIVE, AgentState.ERROR},
|
|
746
|
+
AgentState.IDLE: {AgentState.ACTIVE, AgentState.STOPPED},
|
|
747
|
+
AgentState.ERROR: {AgentState.ACTIVE, AgentState.STOPPED},
|
|
748
|
+
AgentState.STOPPED: set(), # Terminal state
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
if new_state not in valid_transitions.get(self._state, set()):
|
|
752
|
+
raise InvalidStateTransitionError(
|
|
753
|
+
agent_id=self.agent_id,
|
|
754
|
+
current_state=self._state.value,
|
|
755
|
+
attempted_state=new_state.value,
|
|
756
|
+
)
|
|
757
|
+
|
|
758
|
+
self._previous_state = self._state
|
|
759
|
+
self._state = new_state
|
|
760
|
+
self.updated_at = datetime.utcnow()
|
|
761
|
+
|
|
762
|
+
logger.info(f"Agent {self.agent_id} state: {self._previous_state.value} → {new_state.value}")
|
|
763
|
+
|
|
764
|
+
# ==================== Lifecycle Methods ====================
|
|
765
|
+
|
|
766
|
+
async def initialize(self) -> None:
|
|
767
|
+
"""
|
|
768
|
+
Initialize the agent.
|
|
769
|
+
|
|
770
|
+
This method should be called before the agent can be used.
|
|
771
|
+
Override in subclasses to add initialization logic.
|
|
772
|
+
|
|
773
|
+
Raises:
|
|
774
|
+
AgentInitializationError: If initialization fails
|
|
775
|
+
"""
|
|
776
|
+
try:
|
|
777
|
+
self._transition_state(AgentState.INITIALIZING)
|
|
778
|
+
logger.info(f"Initializing agent {self.agent_id}...")
|
|
779
|
+
|
|
780
|
+
# Subclass initialization
|
|
781
|
+
await self._initialize()
|
|
782
|
+
|
|
783
|
+
self._transition_state(AgentState.ACTIVE)
|
|
784
|
+
self.last_active_at = datetime.utcnow()
|
|
785
|
+
logger.info(f"Agent {self.agent_id} initialized successfully")
|
|
786
|
+
|
|
787
|
+
except Exception as e:
|
|
788
|
+
self._transition_state(AgentState.ERROR)
|
|
789
|
+
logger.error(f"Agent {self.agent_id} initialization failed: {e}")
|
|
790
|
+
raise AgentInitializationError(
|
|
791
|
+
f"Failed to initialize agent {self.agent_id}: {str(e)}",
|
|
792
|
+
agent_id=self.agent_id,
|
|
793
|
+
)
|
|
794
|
+
|
|
795
|
+
@abstractmethod
|
|
796
|
+
async def _initialize(self) -> None:
|
|
797
|
+
"""
|
|
798
|
+
Subclass-specific initialization logic.
|
|
799
|
+
|
|
800
|
+
Override this method in subclasses to implement
|
|
801
|
+
custom initialization.
|
|
802
|
+
"""
|
|
803
|
+
|
|
804
|
+
async def activate(self) -> None:
|
|
805
|
+
"""Activate the agent."""
|
|
806
|
+
if self._state == AgentState.IDLE:
|
|
807
|
+
self._transition_state(AgentState.ACTIVE)
|
|
808
|
+
self.last_active_at = datetime.utcnow()
|
|
809
|
+
logger.info(f"Agent {self.agent_id} activated")
|
|
810
|
+
else:
|
|
811
|
+
logger.warning(f"Agent {self.agent_id} cannot be activated from state {self._state.value}")
|
|
812
|
+
|
|
813
|
+
async def deactivate(self) -> None:
|
|
814
|
+
"""Deactivate the agent (enter idle state)."""
|
|
815
|
+
if self._state == AgentState.ACTIVE:
|
|
816
|
+
self._transition_state(AgentState.IDLE)
|
|
817
|
+
logger.info(f"Agent {self.agent_id} deactivated")
|
|
818
|
+
else:
|
|
819
|
+
logger.warning(f"Agent {self.agent_id} cannot be deactivated from state {self._state.value}")
|
|
820
|
+
|
|
821
|
+
async def shutdown(self) -> None:
|
|
822
|
+
"""
|
|
823
|
+
Shutdown the agent.
|
|
824
|
+
|
|
825
|
+
Override in subclasses to add cleanup logic.
|
|
826
|
+
"""
|
|
827
|
+
logger.info(f"Shutting down agent {self.agent_id}...")
|
|
828
|
+
await self._shutdown()
|
|
829
|
+
self._transition_state(AgentState.STOPPED)
|
|
830
|
+
logger.info(f"Agent {self.agent_id} shut down")
|
|
831
|
+
|
|
832
|
+
@abstractmethod
|
|
833
|
+
async def _shutdown(self) -> None:
|
|
834
|
+
"""
|
|
835
|
+
Subclass-specific shutdown logic.
|
|
836
|
+
|
|
837
|
+
Override this method in subclasses to implement
|
|
838
|
+
custom cleanup.
|
|
839
|
+
"""
|
|
840
|
+
|
|
841
|
+
# ==================== Tool and LLM Client Helper Methods ====================
|
|
842
|
+
|
|
843
|
+
def _load_tools(self) -> None:
|
|
844
|
+
"""
|
|
845
|
+
Load tools from the tools input parameter.
|
|
846
|
+
|
|
847
|
+
Handles both List[str] (tool names) and Dict[str, BaseTool] (tool instances).
|
|
848
|
+
Sets _available_tools and _tool_instances appropriately.
|
|
849
|
+
|
|
850
|
+
This helper method should be called by subclasses during initialization
|
|
851
|
+
if they want to use BaseAIAgent's tool management.
|
|
852
|
+
|
|
853
|
+
Raises:
|
|
854
|
+
ConfigurationError: If tools input is invalid
|
|
855
|
+
"""
|
|
856
|
+
if self._tools_input is None:
|
|
857
|
+
# No tools provided
|
|
858
|
+
return
|
|
859
|
+
|
|
860
|
+
if isinstance(self._tools_input, list):
|
|
861
|
+
# Tool names - store for subclass to load
|
|
862
|
+
self._available_tools = self._tools_input
|
|
863
|
+
logger.debug(f"Agent {self.agent_id}: Registered {len(self._tools_input)} tool names")
|
|
864
|
+
|
|
865
|
+
elif isinstance(self._tools_input, dict):
|
|
866
|
+
# Tool instances - validate and store
|
|
867
|
+
from aiecs.tools.base_tool import BaseTool
|
|
868
|
+
|
|
869
|
+
for tool_name, tool_instance in self._tools_input.items():
|
|
870
|
+
if not isinstance(tool_instance, BaseTool):
|
|
871
|
+
raise ConfigurationError(f"Tool '{tool_name}' must be a BaseTool instance, got {type(tool_instance)}")
|
|
872
|
+
|
|
873
|
+
self._tool_instances = self._tools_input
|
|
874
|
+
self._available_tools = list(self._tools_input.keys())
|
|
875
|
+
logger.debug(f"Agent {self.agent_id}: Registered {len(self._tools_input)} tool instances")
|
|
876
|
+
|
|
877
|
+
else:
|
|
878
|
+
raise ConfigurationError(f"Tools must be List[str] or Dict[str, BaseTool], got {type(self._tools_input)}")
|
|
879
|
+
|
|
880
|
+
def _validate_llm_client(self) -> None:
|
|
881
|
+
"""
|
|
882
|
+
Validate that the LLM client implements the required protocol.
|
|
883
|
+
|
|
884
|
+
Checks that the LLM client has the required methods:
|
|
885
|
+
- generate_text
|
|
886
|
+
- stream_text
|
|
887
|
+
- close
|
|
888
|
+
- provider_name (property)
|
|
889
|
+
|
|
890
|
+
This helper method should be called by subclasses during initialization
|
|
891
|
+
if they want to use BaseAIAgent's LLM client validation.
|
|
892
|
+
|
|
893
|
+
Raises:
|
|
894
|
+
ConfigurationError: If LLM client doesn't implement required methods
|
|
895
|
+
"""
|
|
896
|
+
if self._llm_client is None:
|
|
897
|
+
return
|
|
898
|
+
|
|
899
|
+
required_methods = ["generate_text", "stream_text", "close"]
|
|
900
|
+
required_properties = ["provider_name"]
|
|
901
|
+
|
|
902
|
+
for method_name in required_methods:
|
|
903
|
+
if not hasattr(self._llm_client, method_name):
|
|
904
|
+
raise ConfigurationError(f"LLM client must implement '{method_name}' method")
|
|
905
|
+
if not callable(getattr(self._llm_client, method_name)):
|
|
906
|
+
raise ConfigurationError(f"LLM client '{method_name}' must be callable")
|
|
907
|
+
|
|
908
|
+
for prop_name in required_properties:
|
|
909
|
+
if not hasattr(self._llm_client, prop_name):
|
|
910
|
+
raise ConfigurationError(f"LLM client must have '{prop_name}' property")
|
|
911
|
+
|
|
912
|
+
logger.debug(f"Agent {self.agent_id}: LLM client validated successfully")
|
|
913
|
+
|
|
914
|
+
def _get_tool_instances(self) -> Optional[Dict[str, "BaseTool"]]:
|
|
915
|
+
"""
|
|
916
|
+
Get tool instances dictionary.
|
|
917
|
+
|
|
918
|
+
Returns:
|
|
919
|
+
Dictionary of tool instances, or None if no tool instances available
|
|
920
|
+
"""
|
|
921
|
+
return self._tool_instances
|
|
922
|
+
|
|
923
|
+
def get_config_manager(self) -> Optional["ConfigManagerProtocol"]:
|
|
924
|
+
"""
|
|
925
|
+
Get the configuration manager.
|
|
926
|
+
|
|
927
|
+
Returns:
|
|
928
|
+
Configuration manager instance, or None if not configured
|
|
929
|
+
"""
|
|
930
|
+
return self._config_manager
|
|
931
|
+
|
|
932
|
+
# ==================== Abstract Execution Methods ====================
|
|
933
|
+
|
|
934
|
+
@abstractmethod
|
|
935
|
+
async def execute_task(self, task: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
|
|
936
|
+
"""
|
|
937
|
+
Execute a task.
|
|
938
|
+
|
|
939
|
+
Args:
|
|
940
|
+
task: Task specification
|
|
941
|
+
context: Execution context
|
|
942
|
+
|
|
943
|
+
Returns:
|
|
944
|
+
Task execution result
|
|
945
|
+
|
|
946
|
+
Raises:
|
|
947
|
+
TaskExecutionError: If task execution fails
|
|
948
|
+
|
|
949
|
+
Note:
|
|
950
|
+
Subclasses can use `_execute_with_retry()` to wrap task execution
|
|
951
|
+
with automatic retry logic based on agent configuration.
|
|
952
|
+
"""
|
|
953
|
+
|
|
954
|
+
@abstractmethod
|
|
955
|
+
async def process_message(self, message: str, sender_id: Optional[str] = None) -> Dict[str, Any]:
|
|
956
|
+
"""
|
|
957
|
+
Process an incoming message.
|
|
958
|
+
|
|
959
|
+
Args:
|
|
960
|
+
message: Message content
|
|
961
|
+
sender_id: Optional sender identifier
|
|
962
|
+
|
|
963
|
+
Returns:
|
|
964
|
+
Response dictionary
|
|
965
|
+
|
|
966
|
+
Note:
|
|
967
|
+
Subclasses can use `_execute_with_retry()` to wrap message processing
|
|
968
|
+
with automatic retry logic based on agent configuration.
|
|
969
|
+
"""
|
|
970
|
+
|
|
971
|
+
# ==================== Retry Logic Integration ====================
|
|
972
|
+
|
|
973
|
+
async def _execute_with_retry(self, func: Callable, *args, **kwargs) -> Any:
|
|
974
|
+
"""
|
|
975
|
+
Execute a function with retry logic using agent's retry policy.
|
|
976
|
+
|
|
977
|
+
This helper method wraps function execution with automatic retry based on
|
|
978
|
+
the agent's configuration. It uses EnhancedRetryPolicy for sophisticated
|
|
979
|
+
error handling with exponential backoff and error classification.
|
|
980
|
+
|
|
981
|
+
Args:
|
|
982
|
+
func: Async function to execute
|
|
983
|
+
*args: Function positional arguments
|
|
984
|
+
**kwargs: Function keyword arguments
|
|
985
|
+
|
|
986
|
+
Returns:
|
|
987
|
+
Function result
|
|
988
|
+
|
|
989
|
+
Raises:
|
|
990
|
+
Exception: If all retries are exhausted
|
|
991
|
+
|
|
992
|
+
Example:
|
|
993
|
+
```python
|
|
994
|
+
async def _execute_task_internal(self, task, context):
|
|
995
|
+
# Actual task execution logic
|
|
996
|
+
return result
|
|
997
|
+
|
|
998
|
+
async def execute_task(self, task, context):
|
|
999
|
+
return await self._execute_with_retry(
|
|
1000
|
+
self._execute_task_internal,
|
|
1001
|
+
task,
|
|
1002
|
+
context
|
|
1003
|
+
)
|
|
1004
|
+
```
|
|
1005
|
+
"""
|
|
1006
|
+
from .integration.retry_policy import EnhancedRetryPolicy
|
|
1007
|
+
|
|
1008
|
+
# Get retry policy from configuration
|
|
1009
|
+
retry_config = self._config.retry_policy
|
|
1010
|
+
|
|
1011
|
+
# Create retry policy instance
|
|
1012
|
+
retry_policy = EnhancedRetryPolicy(
|
|
1013
|
+
max_retries=retry_config.max_retries,
|
|
1014
|
+
base_delay=retry_config.base_delay,
|
|
1015
|
+
max_delay=retry_config.max_delay,
|
|
1016
|
+
exponential_base=retry_config.exponential_factor,
|
|
1017
|
+
jitter=retry_config.jitter_factor > 0,
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
# Execute with retry
|
|
1021
|
+
return await retry_policy.execute_with_retry(func, *args, **kwargs)
|
|
1022
|
+
|
|
1023
|
+
# ==================== Memory Management ====================
|
|
1024
|
+
|
|
1025
|
+
async def add_to_memory(
|
|
1026
|
+
self,
|
|
1027
|
+
key: str,
|
|
1028
|
+
value: Any,
|
|
1029
|
+
memory_type: MemoryType = MemoryType.SHORT_TERM,
|
|
1030
|
+
metadata: Optional[Dict[str, Any]] = None,
|
|
1031
|
+
) -> None:
|
|
1032
|
+
"""
|
|
1033
|
+
Add an item to agent memory.
|
|
1034
|
+
|
|
1035
|
+
Args:
|
|
1036
|
+
key: Memory key
|
|
1037
|
+
value: Memory value
|
|
1038
|
+
memory_type: Type of memory (short_term or long_term)
|
|
1039
|
+
metadata: Optional metadata
|
|
1040
|
+
"""
|
|
1041
|
+
self._memory[key] = value
|
|
1042
|
+
self._memory_metadata[key] = {
|
|
1043
|
+
"type": memory_type.value,
|
|
1044
|
+
"timestamp": datetime.utcnow(),
|
|
1045
|
+
"metadata": metadata or {},
|
|
1046
|
+
}
|
|
1047
|
+
logger.debug(f"Agent {self.agent_id} added memory: {key} ({memory_type.value})")
|
|
1048
|
+
|
|
1049
|
+
async def retrieve_memory(self, key: str, default: Any = None) -> Any:
|
|
1050
|
+
"""
|
|
1051
|
+
Retrieve an item from memory.
|
|
1052
|
+
|
|
1053
|
+
Args:
|
|
1054
|
+
key: Memory key
|
|
1055
|
+
default: Default value if key not found
|
|
1056
|
+
|
|
1057
|
+
Returns:
|
|
1058
|
+
Memory value or default
|
|
1059
|
+
"""
|
|
1060
|
+
return self._memory.get(key, default)
|
|
1061
|
+
|
|
1062
|
+
async def clear_memory(self, memory_type: Optional[MemoryType] = None) -> None:
|
|
1063
|
+
"""
|
|
1064
|
+
Clear agent memory.
|
|
1065
|
+
|
|
1066
|
+
Args:
|
|
1067
|
+
memory_type: If specified, clear only this type of memory
|
|
1068
|
+
"""
|
|
1069
|
+
if memory_type is None:
|
|
1070
|
+
self._memory.clear()
|
|
1071
|
+
self._memory_metadata.clear()
|
|
1072
|
+
logger.info(f"Agent {self.agent_id} cleared all memory")
|
|
1073
|
+
else:
|
|
1074
|
+
keys_to_remove = [k for k, v in self._memory_metadata.items() if v.get("type") == memory_type.value]
|
|
1075
|
+
for key in keys_to_remove:
|
|
1076
|
+
del self._memory[key]
|
|
1077
|
+
del self._memory_metadata[key]
|
|
1078
|
+
logger.info(f"Agent {self.agent_id} cleared {memory_type.value} memory")
|
|
1079
|
+
|
|
1080
|
+
def get_memory_summary(self) -> Dict[str, Any]:
|
|
1081
|
+
"""Get a summary of agent memory."""
|
|
1082
|
+
return {
|
|
1083
|
+
"total_items": len(self._memory),
|
|
1084
|
+
"short_term_count": sum(1 for v in self._memory_metadata.values() if v.get("type") == MemoryType.SHORT_TERM.value),
|
|
1085
|
+
"long_term_count": sum(1 for v in self._memory_metadata.values() if v.get("type") == MemoryType.LONG_TERM.value),
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
# ==================== Goal Management ====================
|
|
1089
|
+
|
|
1090
|
+
def set_goal(
|
|
1091
|
+
self,
|
|
1092
|
+
description: str,
|
|
1093
|
+
priority: GoalPriority = GoalPriority.MEDIUM,
|
|
1094
|
+
success_criteria: Optional[str] = None,
|
|
1095
|
+
deadline: Optional[datetime] = None,
|
|
1096
|
+
) -> str:
|
|
1097
|
+
"""
|
|
1098
|
+
Set a new goal for the agent.
|
|
1099
|
+
|
|
1100
|
+
Args:
|
|
1101
|
+
description: Goal description
|
|
1102
|
+
priority: Goal priority
|
|
1103
|
+
success_criteria: Success criteria
|
|
1104
|
+
deadline: Goal deadline
|
|
1105
|
+
|
|
1106
|
+
Returns:
|
|
1107
|
+
Goal ID
|
|
1108
|
+
"""
|
|
1109
|
+
goal = AgentGoal( # type: ignore[call-arg]
|
|
1110
|
+
description=description,
|
|
1111
|
+
priority=priority,
|
|
1112
|
+
success_criteria=success_criteria,
|
|
1113
|
+
deadline=deadline,
|
|
1114
|
+
)
|
|
1115
|
+
self._goals[goal.goal_id] = goal
|
|
1116
|
+
logger.info(f"Agent {self.agent_id} set goal: {goal.goal_id} ({priority.value})")
|
|
1117
|
+
return goal.goal_id
|
|
1118
|
+
|
|
1119
|
+
def get_goals(self, status: Optional[GoalStatus] = None) -> List[AgentGoal]:
|
|
1120
|
+
"""
|
|
1121
|
+
Get agent goals.
|
|
1122
|
+
|
|
1123
|
+
Args:
|
|
1124
|
+
status: Filter by status (optional)
|
|
1125
|
+
|
|
1126
|
+
Returns:
|
|
1127
|
+
List of goals
|
|
1128
|
+
"""
|
|
1129
|
+
if status is None:
|
|
1130
|
+
return list(self._goals.values())
|
|
1131
|
+
return [g for g in self._goals.values() if g.status == status]
|
|
1132
|
+
|
|
1133
|
+
def get_goal(self, goal_id: str) -> Optional[AgentGoal]:
|
|
1134
|
+
"""Get a specific goal by ID."""
|
|
1135
|
+
return self._goals.get(goal_id)
|
|
1136
|
+
|
|
1137
|
+
def update_goal_status(
|
|
1138
|
+
self,
|
|
1139
|
+
goal_id: str,
|
|
1140
|
+
status: GoalStatus,
|
|
1141
|
+
progress: Optional[float] = None,
|
|
1142
|
+
) -> None:
|
|
1143
|
+
"""
|
|
1144
|
+
Update goal status.
|
|
1145
|
+
|
|
1146
|
+
Args:
|
|
1147
|
+
goal_id: Goal ID
|
|
1148
|
+
status: New status
|
|
1149
|
+
progress: Optional progress percentage
|
|
1150
|
+
"""
|
|
1151
|
+
if goal_id not in self._goals:
|
|
1152
|
+
logger.warning(f"Goal {goal_id} not found for agent {self.agent_id}")
|
|
1153
|
+
return
|
|
1154
|
+
|
|
1155
|
+
goal = self._goals[goal_id]
|
|
1156
|
+
goal.status = status
|
|
1157
|
+
|
|
1158
|
+
if progress is not None:
|
|
1159
|
+
goal.progress = progress
|
|
1160
|
+
|
|
1161
|
+
if status == GoalStatus.IN_PROGRESS and goal.started_at is None:
|
|
1162
|
+
goal.started_at = datetime.utcnow()
|
|
1163
|
+
elif status == GoalStatus.ACHIEVED:
|
|
1164
|
+
goal.achieved_at = datetime.utcnow()
|
|
1165
|
+
|
|
1166
|
+
logger.info(f"Agent {self.agent_id} updated goal {goal_id}: {status.value}")
|
|
1167
|
+
|
|
1168
|
+
# ==================== Configuration Management ====================
|
|
1169
|
+
|
|
1170
|
+
def get_config(self) -> AgentConfiguration:
|
|
1171
|
+
"""Get agent configuration."""
|
|
1172
|
+
return self._config
|
|
1173
|
+
|
|
1174
|
+
def update_config(self, updates: Dict[str, Any]) -> None:
|
|
1175
|
+
"""
|
|
1176
|
+
Update agent configuration.
|
|
1177
|
+
|
|
1178
|
+
Args:
|
|
1179
|
+
updates: Configuration updates
|
|
1180
|
+
|
|
1181
|
+
Raises:
|
|
1182
|
+
ConfigurationError: If configuration is invalid
|
|
1183
|
+
"""
|
|
1184
|
+
try:
|
|
1185
|
+
# Update configuration
|
|
1186
|
+
for key, value in updates.items():
|
|
1187
|
+
if hasattr(self._config, key):
|
|
1188
|
+
setattr(self._config, key, value)
|
|
1189
|
+
else:
|
|
1190
|
+
logger.warning(f"Unknown config key: {key}")
|
|
1191
|
+
|
|
1192
|
+
self.updated_at = datetime.utcnow()
|
|
1193
|
+
logger.info(f"Agent {self.agent_id} configuration updated")
|
|
1194
|
+
|
|
1195
|
+
except Exception as e:
|
|
1196
|
+
raise ConfigurationError(
|
|
1197
|
+
f"Failed to update configuration: {str(e)}",
|
|
1198
|
+
agent_id=self.agent_id,
|
|
1199
|
+
)
|
|
1200
|
+
|
|
1201
|
+
# ==================== Capability Management ====================
|
|
1202
|
+
|
|
1203
|
+
def declare_capability(
|
|
1204
|
+
self,
|
|
1205
|
+
capability_type: str,
|
|
1206
|
+
level: str,
|
|
1207
|
+
description: Optional[str] = None,
|
|
1208
|
+
constraints: Optional[Dict[str, Any]] = None,
|
|
1209
|
+
) -> None:
|
|
1210
|
+
"""
|
|
1211
|
+
Declare an agent capability.
|
|
1212
|
+
|
|
1213
|
+
Args:
|
|
1214
|
+
capability_type: Type of capability
|
|
1215
|
+
level: Proficiency level
|
|
1216
|
+
description: Capability description
|
|
1217
|
+
constraints: Capability constraints
|
|
1218
|
+
"""
|
|
1219
|
+
from .models import CapabilityLevel
|
|
1220
|
+
|
|
1221
|
+
capability = AgentCapabilityDeclaration(
|
|
1222
|
+
capability_type=capability_type,
|
|
1223
|
+
level=CapabilityLevel(level),
|
|
1224
|
+
description=description,
|
|
1225
|
+
constraints=constraints or {},
|
|
1226
|
+
)
|
|
1227
|
+
self._capabilities[capability_type] = capability
|
|
1228
|
+
logger.info(f"Agent {self.agent_id} declared capability: {capability_type} ({level})")
|
|
1229
|
+
|
|
1230
|
+
def has_capability(self, capability_type: str) -> bool:
|
|
1231
|
+
"""Check if agent has a capability."""
|
|
1232
|
+
return capability_type in self._capabilities
|
|
1233
|
+
|
|
1234
|
+
def get_capabilities(self) -> List[AgentCapabilityDeclaration]:
|
|
1235
|
+
"""Get all agent capabilities."""
|
|
1236
|
+
return list(self._capabilities.values())
|
|
1237
|
+
|
|
1238
|
+
# ==================== Metrics Tracking ====================
|
|
1239
|
+
|
|
1240
|
+
def get_metrics(self) -> AgentMetrics:
|
|
1241
|
+
"""Get agent metrics."""
|
|
1242
|
+
return self._metrics
|
|
1243
|
+
|
|
1244
|
+
def update_metrics(
|
|
1245
|
+
self,
|
|
1246
|
+
execution_time: Optional[float] = None,
|
|
1247
|
+
success: bool = True,
|
|
1248
|
+
quality_score: Optional[float] = None,
|
|
1249
|
+
tokens_used: Optional[int] = None,
|
|
1250
|
+
tool_calls: Optional[int] = None,
|
|
1251
|
+
) -> None:
|
|
1252
|
+
"""
|
|
1253
|
+
Update agent metrics.
|
|
1254
|
+
|
|
1255
|
+
Args:
|
|
1256
|
+
execution_time: Task execution time
|
|
1257
|
+
success: Whether task succeeded
|
|
1258
|
+
quality_score: Quality score (0-1)
|
|
1259
|
+
tokens_used: Tokens used
|
|
1260
|
+
tool_calls: Number of tool calls
|
|
1261
|
+
"""
|
|
1262
|
+
self._metrics.total_tasks_executed += 1
|
|
1263
|
+
|
|
1264
|
+
if success:
|
|
1265
|
+
self._metrics.successful_tasks += 1
|
|
1266
|
+
else:
|
|
1267
|
+
self._metrics.failed_tasks += 1
|
|
1268
|
+
|
|
1269
|
+
# Update success rate
|
|
1270
|
+
self._metrics.success_rate = self._metrics.successful_tasks / self._metrics.total_tasks_executed * 100
|
|
1271
|
+
|
|
1272
|
+
# Update execution time
|
|
1273
|
+
if execution_time is not None:
|
|
1274
|
+
self._metrics.total_execution_time += execution_time
|
|
1275
|
+
self._metrics.average_execution_time = self._metrics.total_execution_time / self._metrics.total_tasks_executed
|
|
1276
|
+
|
|
1277
|
+
if self._metrics.min_execution_time is None or execution_time < self._metrics.min_execution_time:
|
|
1278
|
+
self._metrics.min_execution_time = execution_time
|
|
1279
|
+
if self._metrics.max_execution_time is None or execution_time > self._metrics.max_execution_time:
|
|
1280
|
+
self._metrics.max_execution_time = execution_time
|
|
1281
|
+
|
|
1282
|
+
# Update quality score
|
|
1283
|
+
if quality_score is not None:
|
|
1284
|
+
if self._metrics.average_quality_score is None:
|
|
1285
|
+
self._metrics.average_quality_score = quality_score
|
|
1286
|
+
else:
|
|
1287
|
+
# Running average
|
|
1288
|
+
total_quality = self._metrics.average_quality_score * (self._metrics.total_tasks_executed - 1)
|
|
1289
|
+
self._metrics.average_quality_score = (total_quality + quality_score) / self._metrics.total_tasks_executed
|
|
1290
|
+
|
|
1291
|
+
# Update resource usage
|
|
1292
|
+
if tokens_used is not None:
|
|
1293
|
+
self._metrics.total_tokens_used += tokens_used
|
|
1294
|
+
if tool_calls is not None:
|
|
1295
|
+
self._metrics.total_tool_calls += tool_calls
|
|
1296
|
+
|
|
1297
|
+
self._metrics.updated_at = datetime.utcnow()
|
|
1298
|
+
|
|
1299
|
+
def update_cache_metrics(
|
|
1300
|
+
self,
|
|
1301
|
+
cache_read_tokens: Optional[int] = None,
|
|
1302
|
+
cache_creation_tokens: Optional[int] = None,
|
|
1303
|
+
cache_hit: Optional[bool] = None,
|
|
1304
|
+
) -> None:
|
|
1305
|
+
"""
|
|
1306
|
+
Update prompt cache metrics from LLM response.
|
|
1307
|
+
|
|
1308
|
+
This method tracks provider-level prompt caching statistics to monitor
|
|
1309
|
+
cache hit rates and token savings.
|
|
1310
|
+
|
|
1311
|
+
Args:
|
|
1312
|
+
cache_read_tokens: Tokens read from cache (indicates cache hit)
|
|
1313
|
+
cache_creation_tokens: Tokens used to create a new cache entry
|
|
1314
|
+
cache_hit: Whether the request hit a cached prompt prefix
|
|
1315
|
+
|
|
1316
|
+
Example:
|
|
1317
|
+
# After receiving LLM response
|
|
1318
|
+
agent.update_cache_metrics(
|
|
1319
|
+
cache_read_tokens=response.cache_read_tokens,
|
|
1320
|
+
cache_creation_tokens=response.cache_creation_tokens,
|
|
1321
|
+
cache_hit=response.cache_hit
|
|
1322
|
+
)
|
|
1323
|
+
"""
|
|
1324
|
+
# Track LLM request count
|
|
1325
|
+
self._metrics.total_llm_requests += 1
|
|
1326
|
+
|
|
1327
|
+
# Track cache hit/miss
|
|
1328
|
+
if cache_hit is True:
|
|
1329
|
+
self._metrics.cache_hits += 1
|
|
1330
|
+
elif cache_hit is False:
|
|
1331
|
+
self._metrics.cache_misses += 1
|
|
1332
|
+
elif cache_read_tokens is not None and cache_read_tokens > 0:
|
|
1333
|
+
# Infer cache hit from tokens
|
|
1334
|
+
self._metrics.cache_hits += 1
|
|
1335
|
+
elif cache_creation_tokens is not None and cache_creation_tokens > 0:
|
|
1336
|
+
# Infer cache miss from creation tokens
|
|
1337
|
+
self._metrics.cache_misses += 1
|
|
1338
|
+
|
|
1339
|
+
# Update cache hit rate
|
|
1340
|
+
total_cache_requests = self._metrics.cache_hits + self._metrics.cache_misses
|
|
1341
|
+
if total_cache_requests > 0:
|
|
1342
|
+
self._metrics.cache_hit_rate = self._metrics.cache_hits / total_cache_requests
|
|
1343
|
+
|
|
1344
|
+
# Track cache tokens
|
|
1345
|
+
if cache_read_tokens is not None and cache_read_tokens > 0:
|
|
1346
|
+
self._metrics.total_cache_read_tokens += cache_read_tokens
|
|
1347
|
+
# Provider-level caching saves ~90% of token cost for cached tokens
|
|
1348
|
+
self._metrics.estimated_cache_savings_tokens += int(cache_read_tokens * 0.9)
|
|
1349
|
+
|
|
1350
|
+
if cache_creation_tokens is not None and cache_creation_tokens > 0:
|
|
1351
|
+
self._metrics.total_cache_creation_tokens += cache_creation_tokens
|
|
1352
|
+
|
|
1353
|
+
self._metrics.updated_at = datetime.utcnow()
|
|
1354
|
+
logger.debug(
|
|
1355
|
+
f"Agent {self.agent_id} cache metrics updated: "
|
|
1356
|
+
f"hit_rate={self._metrics.cache_hit_rate:.2%}, "
|
|
1357
|
+
f"read_tokens={cache_read_tokens}, creation_tokens={cache_creation_tokens}"
|
|
1358
|
+
)
|
|
1359
|
+
|
|
1360
|
+
def update_session_metrics(
|
|
1361
|
+
self,
|
|
1362
|
+
session_status: str,
|
|
1363
|
+
session_duration: Optional[float] = None,
|
|
1364
|
+
session_requests: int = 0,
|
|
1365
|
+
) -> None:
|
|
1366
|
+
"""
|
|
1367
|
+
Update session-level metrics.
|
|
1368
|
+
|
|
1369
|
+
This method should be called when a session is created, updated, or ended
|
|
1370
|
+
to track session-level statistics in agent metrics.
|
|
1371
|
+
|
|
1372
|
+
Args:
|
|
1373
|
+
session_status: Session status (active, completed, failed, expired)
|
|
1374
|
+
session_duration: Session duration in seconds (for ended sessions)
|
|
1375
|
+
session_requests: Number of requests in the session
|
|
1376
|
+
|
|
1377
|
+
Example:
|
|
1378
|
+
# When creating a session
|
|
1379
|
+
agent.update_session_metrics(session_status="active")
|
|
1380
|
+
|
|
1381
|
+
# When ending a session
|
|
1382
|
+
agent.update_session_metrics(
|
|
1383
|
+
session_status="completed",
|
|
1384
|
+
session_duration=300.5,
|
|
1385
|
+
session_requests=15
|
|
1386
|
+
)
|
|
1387
|
+
"""
|
|
1388
|
+
# Update session counts based on status
|
|
1389
|
+
if session_status == "active":
|
|
1390
|
+
self._metrics.total_sessions += 1
|
|
1391
|
+
self._metrics.active_sessions += 1
|
|
1392
|
+
elif session_status == "completed":
|
|
1393
|
+
self._metrics.completed_sessions += 1
|
|
1394
|
+
if self._metrics.active_sessions > 0:
|
|
1395
|
+
self._metrics.active_sessions -= 1
|
|
1396
|
+
elif session_status == "failed":
|
|
1397
|
+
self._metrics.failed_sessions += 1
|
|
1398
|
+
if self._metrics.active_sessions > 0:
|
|
1399
|
+
self._metrics.active_sessions -= 1
|
|
1400
|
+
elif session_status == "expired":
|
|
1401
|
+
self._metrics.expired_sessions += 1
|
|
1402
|
+
if self._metrics.active_sessions > 0:
|
|
1403
|
+
self._metrics.active_sessions -= 1
|
|
1404
|
+
|
|
1405
|
+
# Update session request tracking
|
|
1406
|
+
if session_requests > 0:
|
|
1407
|
+
self._metrics.total_session_requests += session_requests
|
|
1408
|
+
|
|
1409
|
+
# Update average session duration
|
|
1410
|
+
if session_duration is not None and session_duration > 0:
|
|
1411
|
+
completed_count = self._metrics.completed_sessions + self._metrics.failed_sessions + self._metrics.expired_sessions
|
|
1412
|
+
if completed_count > 0:
|
|
1413
|
+
if self._metrics.average_session_duration is None:
|
|
1414
|
+
self._metrics.average_session_duration = session_duration
|
|
1415
|
+
else:
|
|
1416
|
+
# Running average
|
|
1417
|
+
total_duration = self._metrics.average_session_duration * (completed_count - 1)
|
|
1418
|
+
self._metrics.average_session_duration = (total_duration + session_duration) / completed_count
|
|
1419
|
+
|
|
1420
|
+
# Update average requests per session
|
|
1421
|
+
if self._metrics.total_sessions > 0:
|
|
1422
|
+
self._metrics.average_requests_per_session = self._metrics.total_session_requests / self._metrics.total_sessions
|
|
1423
|
+
|
|
1424
|
+
self._metrics.updated_at = datetime.utcnow()
|
|
1425
|
+
logger.debug(f"Agent {self.agent_id} session metrics updated: " f"status={session_status}, total_sessions={self._metrics.total_sessions}, " f"active_sessions={self._metrics.active_sessions}")
|
|
1426
|
+
|
|
1427
|
+
# ==================== Performance Tracking ====================
|
|
1428
|
+
|
|
1429
|
+
def track_operation_time(self, operation_name: str) -> OperationTimer:
|
|
1430
|
+
"""
|
|
1431
|
+
Create a context manager for tracking operation time.
|
|
1432
|
+
|
|
1433
|
+
This method returns an OperationTimer that automatically records
|
|
1434
|
+
operation duration and updates agent metrics when the operation completes.
|
|
1435
|
+
|
|
1436
|
+
Args:
|
|
1437
|
+
operation_name: Name of the operation to track
|
|
1438
|
+
|
|
1439
|
+
Returns:
|
|
1440
|
+
OperationTimer context manager
|
|
1441
|
+
|
|
1442
|
+
Example:
|
|
1443
|
+
with agent.track_operation_time("llm_call") as timer:
|
|
1444
|
+
result = await llm.generate(prompt)
|
|
1445
|
+
# Metrics are automatically recorded
|
|
1446
|
+
|
|
1447
|
+
# Access duration if needed
|
|
1448
|
+
print(f"Operation took {timer.duration} seconds")
|
|
1449
|
+
"""
|
|
1450
|
+
return OperationTimer(operation_name=operation_name, agent=self)
|
|
1451
|
+
|
|
1452
|
+
def _record_operation_metrics(self, operation_name: str, duration: float, success: bool = True) -> None:
|
|
1453
|
+
"""
|
|
1454
|
+
Record operation-level metrics.
|
|
1455
|
+
|
|
1456
|
+
This method is called automatically by OperationTimer but can also
|
|
1457
|
+
be called manually to record operation metrics.
|
|
1458
|
+
|
|
1459
|
+
Args:
|
|
1460
|
+
operation_name: Name of the operation
|
|
1461
|
+
duration: Operation duration in seconds
|
|
1462
|
+
success: Whether the operation succeeded
|
|
1463
|
+
|
|
1464
|
+
Example:
|
|
1465
|
+
# Manual recording
|
|
1466
|
+
start = time.time()
|
|
1467
|
+
try:
|
|
1468
|
+
result = perform_operation()
|
|
1469
|
+
agent._record_operation_metrics("custom_op", time.time() - start, True)
|
|
1470
|
+
except Exception:
|
|
1471
|
+
agent._record_operation_metrics("custom_op", time.time() - start, False)
|
|
1472
|
+
raise
|
|
1473
|
+
"""
|
|
1474
|
+
# Update operation counts
|
|
1475
|
+
if operation_name not in self._metrics.operation_counts:
|
|
1476
|
+
self._metrics.operation_counts[operation_name] = 0
|
|
1477
|
+
self._metrics.operation_total_time[operation_name] = 0.0
|
|
1478
|
+
self._metrics.operation_error_counts[operation_name] = 0
|
|
1479
|
+
|
|
1480
|
+
self._metrics.operation_counts[operation_name] += 1
|
|
1481
|
+
self._metrics.operation_total_time[operation_name] += duration
|
|
1482
|
+
|
|
1483
|
+
if not success:
|
|
1484
|
+
self._metrics.operation_error_counts[operation_name] += 1
|
|
1485
|
+
|
|
1486
|
+
# Add to operation history (keep last 100 operations)
|
|
1487
|
+
operation_record = {
|
|
1488
|
+
"operation": operation_name,
|
|
1489
|
+
"duration": duration,
|
|
1490
|
+
"success": success,
|
|
1491
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
1492
|
+
}
|
|
1493
|
+
self._metrics.operation_history.append(operation_record)
|
|
1494
|
+
|
|
1495
|
+
# Keep only last 100 operations
|
|
1496
|
+
if len(self._metrics.operation_history) > 100:
|
|
1497
|
+
self._metrics.operation_history = self._metrics.operation_history[-100:]
|
|
1498
|
+
|
|
1499
|
+
# Recalculate percentiles
|
|
1500
|
+
self._update_operation_percentiles()
|
|
1501
|
+
|
|
1502
|
+
self._metrics.updated_at = datetime.utcnow()
|
|
1503
|
+
logger.debug(f"Agent {self.agent_id} operation metrics recorded: " f"operation={operation_name}, duration={duration:.3f}s, success={success}")
|
|
1504
|
+
|
|
1505
|
+
def _update_operation_percentiles(self) -> None:
|
|
1506
|
+
"""Update operation time percentiles from operation history."""
|
|
1507
|
+
if not self._metrics.operation_history:
|
|
1508
|
+
return
|
|
1509
|
+
|
|
1510
|
+
# Extract durations from operation history
|
|
1511
|
+
durations = [op["duration"] for op in self._metrics.operation_history]
|
|
1512
|
+
|
|
1513
|
+
# Calculate percentiles
|
|
1514
|
+
self._metrics.p50_operation_time = self._calculate_percentile(durations, 50)
|
|
1515
|
+
self._metrics.p95_operation_time = self._calculate_percentile(durations, 95)
|
|
1516
|
+
self._metrics.p99_operation_time = self._calculate_percentile(durations, 99)
|
|
1517
|
+
|
|
1518
|
+
def _calculate_percentile(self, values: List[float], percentile: int) -> Optional[float]:
|
|
1519
|
+
"""
|
|
1520
|
+
Calculate percentile from a list of values.
|
|
1521
|
+
|
|
1522
|
+
Args:
|
|
1523
|
+
values: List of numeric values
|
|
1524
|
+
percentile: Percentile to calculate (0-100)
|
|
1525
|
+
|
|
1526
|
+
Returns:
|
|
1527
|
+
Percentile value or None if values is empty
|
|
1528
|
+
|
|
1529
|
+
Example:
|
|
1530
|
+
p95 = agent._calculate_percentile([1.0, 2.0, 3.0, 4.0, 5.0], 95)
|
|
1531
|
+
"""
|
|
1532
|
+
if not values:
|
|
1533
|
+
return None
|
|
1534
|
+
|
|
1535
|
+
sorted_values = sorted(values)
|
|
1536
|
+
index = int(len(sorted_values) * percentile / 100)
|
|
1537
|
+
|
|
1538
|
+
# Handle edge cases
|
|
1539
|
+
if index >= len(sorted_values):
|
|
1540
|
+
index = len(sorted_values) - 1
|
|
1541
|
+
|
|
1542
|
+
return sorted_values[index]
|
|
1543
|
+
|
|
1544
|
+
def get_performance_metrics(self) -> Dict[str, Any]:
|
|
1545
|
+
"""
|
|
1546
|
+
Get comprehensive performance metrics.
|
|
1547
|
+
|
|
1548
|
+
Returns detailed performance statistics including operation-level
|
|
1549
|
+
metrics, percentiles, and aggregated statistics.
|
|
1550
|
+
|
|
1551
|
+
Returns:
|
|
1552
|
+
Dictionary with performance metrics
|
|
1553
|
+
|
|
1554
|
+
Example:
|
|
1555
|
+
metrics = agent.get_performance_metrics()
|
|
1556
|
+
print(f"P95 latency: {metrics['p95_operation_time']}s")
|
|
1557
|
+
print(f"Total operations: {metrics['total_operations']}")
|
|
1558
|
+
for op_name, stats in metrics['operations'].items():
|
|
1559
|
+
print(f"{op_name}: {stats['count']} calls, avg {stats['avg_time']:.3f}s")
|
|
1560
|
+
"""
|
|
1561
|
+
# Calculate per-operation statistics
|
|
1562
|
+
operations = {}
|
|
1563
|
+
for op_name, count in self._metrics.operation_counts.items():
|
|
1564
|
+
total_time = self._metrics.operation_total_time.get(op_name, 0.0)
|
|
1565
|
+
error_count = self._metrics.operation_error_counts.get(op_name, 0)
|
|
1566
|
+
|
|
1567
|
+
operations[op_name] = {
|
|
1568
|
+
"count": count,
|
|
1569
|
+
"total_time": total_time,
|
|
1570
|
+
"average_time": total_time / count if count > 0 else 0.0,
|
|
1571
|
+
"error_count": error_count,
|
|
1572
|
+
"error_rate": (error_count / count * 100) if count > 0 else 0.0,
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
return {
|
|
1576
|
+
"total_operations": sum(self._metrics.operation_counts.values()),
|
|
1577
|
+
"operations": operations,
|
|
1578
|
+
"p50_operation_time": self._metrics.p50_operation_time,
|
|
1579
|
+
"p95_operation_time": self._metrics.p95_operation_time,
|
|
1580
|
+
"p99_operation_time": self._metrics.p99_operation_time,
|
|
1581
|
+
"recent_operations": self._metrics.operation_history[-10:], # Last 10 operations
|
|
1582
|
+
# Prompt cache metrics
|
|
1583
|
+
"prompt_cache": {
|
|
1584
|
+
"total_llm_requests": self._metrics.total_llm_requests,
|
|
1585
|
+
"cache_hits": self._metrics.cache_hits,
|
|
1586
|
+
"cache_misses": self._metrics.cache_misses,
|
|
1587
|
+
"cache_hit_rate": self._metrics.cache_hit_rate,
|
|
1588
|
+
"cache_hit_rate_pct": f"{self._metrics.cache_hit_rate * 100:.1f}%",
|
|
1589
|
+
"total_cache_read_tokens": self._metrics.total_cache_read_tokens,
|
|
1590
|
+
"total_cache_creation_tokens": self._metrics.total_cache_creation_tokens,
|
|
1591
|
+
"estimated_cache_savings_tokens": self._metrics.estimated_cache_savings_tokens,
|
|
1592
|
+
"estimated_cache_savings_cost": self._metrics.estimated_cache_savings_cost,
|
|
1593
|
+
},
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
def get_health_status(self) -> Dict[str, Any]:
|
|
1597
|
+
"""
|
|
1598
|
+
Get agent health status with health score calculation.
|
|
1599
|
+
|
|
1600
|
+
Calculates a health score (0-100) based on multiple factors:
|
|
1601
|
+
- Success rate (40% weight)
|
|
1602
|
+
- Error rate (30% weight)
|
|
1603
|
+
- Performance (20% weight)
|
|
1604
|
+
- Session health (10% weight)
|
|
1605
|
+
|
|
1606
|
+
Returns:
|
|
1607
|
+
Dictionary with health status and score
|
|
1608
|
+
|
|
1609
|
+
Example:
|
|
1610
|
+
health = agent.get_health_status()
|
|
1611
|
+
print(f"Health score: {health['health_score']}/100")
|
|
1612
|
+
print(f"Status: {health['status']}") # healthy, degraded, unhealthy
|
|
1613
|
+
if health['issues']:
|
|
1614
|
+
print(f"Issues: {', '.join(health['issues'])}")
|
|
1615
|
+
"""
|
|
1616
|
+
issues = []
|
|
1617
|
+
health_score = 100.0
|
|
1618
|
+
|
|
1619
|
+
# Factor 1: Success rate (40% weight)
|
|
1620
|
+
success_rate = self._metrics.success_rate
|
|
1621
|
+
if success_rate < 50:
|
|
1622
|
+
issues.append("Low success rate")
|
|
1623
|
+
health_score -= 40
|
|
1624
|
+
elif success_rate < 80:
|
|
1625
|
+
issues.append("Moderate success rate")
|
|
1626
|
+
health_score -= 20
|
|
1627
|
+
elif success_rate < 95:
|
|
1628
|
+
health_score -= 10
|
|
1629
|
+
|
|
1630
|
+
# Factor 2: Error rate (30% weight)
|
|
1631
|
+
total_tasks = self._metrics.total_tasks_executed
|
|
1632
|
+
if total_tasks > 0:
|
|
1633
|
+
error_rate = (self._metrics.failed_tasks / total_tasks) * 100
|
|
1634
|
+
if error_rate > 50:
|
|
1635
|
+
issues.append("High error rate")
|
|
1636
|
+
health_score -= 30
|
|
1637
|
+
elif error_rate > 20:
|
|
1638
|
+
issues.append("Elevated error rate")
|
|
1639
|
+
health_score -= 15
|
|
1640
|
+
elif error_rate > 5:
|
|
1641
|
+
health_score -= 5
|
|
1642
|
+
|
|
1643
|
+
# Factor 3: Performance (20% weight)
|
|
1644
|
+
if self._metrics.p95_operation_time is not None:
|
|
1645
|
+
# Consider p95 > 5s as slow
|
|
1646
|
+
if self._metrics.p95_operation_time > 10:
|
|
1647
|
+
issues.append("Very slow operations (p95 > 10s)")
|
|
1648
|
+
health_score -= 20
|
|
1649
|
+
elif self._metrics.p95_operation_time > 5:
|
|
1650
|
+
issues.append("Slow operations (p95 > 5s)")
|
|
1651
|
+
health_score -= 10
|
|
1652
|
+
|
|
1653
|
+
# Factor 4: Session health (10% weight)
|
|
1654
|
+
if self._metrics.total_sessions > 0:
|
|
1655
|
+
session_failure_rate = (self._metrics.failed_sessions + self._metrics.expired_sessions) / self._metrics.total_sessions * 100
|
|
1656
|
+
if session_failure_rate > 30:
|
|
1657
|
+
issues.append("High session failure rate")
|
|
1658
|
+
health_score -= 10
|
|
1659
|
+
elif session_failure_rate > 10:
|
|
1660
|
+
health_score -= 5
|
|
1661
|
+
|
|
1662
|
+
# Ensure health score is in valid range
|
|
1663
|
+
health_score = max(0.0, min(100.0, health_score))
|
|
1664
|
+
|
|
1665
|
+
# Determine status
|
|
1666
|
+
if health_score >= 80:
|
|
1667
|
+
status = "healthy"
|
|
1668
|
+
elif health_score >= 50:
|
|
1669
|
+
status = "degraded"
|
|
1670
|
+
else:
|
|
1671
|
+
status = "unhealthy"
|
|
1672
|
+
|
|
1673
|
+
return {
|
|
1674
|
+
"health_score": health_score,
|
|
1675
|
+
"status": status,
|
|
1676
|
+
"issues": issues,
|
|
1677
|
+
"metrics_summary": {
|
|
1678
|
+
"success_rate": success_rate,
|
|
1679
|
+
"total_tasks": total_tasks,
|
|
1680
|
+
"total_sessions": self._metrics.total_sessions,
|
|
1681
|
+
"active_sessions": self._metrics.active_sessions,
|
|
1682
|
+
"p95_operation_time": self._metrics.p95_operation_time,
|
|
1683
|
+
},
|
|
1684
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
def get_comprehensive_status(self) -> Dict[str, Any]:
|
|
1688
|
+
"""
|
|
1689
|
+
Get comprehensive agent status combining all metrics.
|
|
1690
|
+
|
|
1691
|
+
Returns a complete view of agent state, health, performance,
|
|
1692
|
+
and operational metrics.
|
|
1693
|
+
|
|
1694
|
+
Returns:
|
|
1695
|
+
Dictionary with comprehensive status information
|
|
1696
|
+
|
|
1697
|
+
Example:
|
|
1698
|
+
status = agent.get_comprehensive_status()
|
|
1699
|
+
print(f"Agent: {status['agent_id']}")
|
|
1700
|
+
print(f"State: {status['state']}")
|
|
1701
|
+
print(f"Health: {status['health']['status']} ({status['health']['health_score']}/100)")
|
|
1702
|
+
print(f"Tasks: {status['metrics']['total_tasks_executed']}")
|
|
1703
|
+
print(f"Sessions: {status['metrics']['total_sessions']}")
|
|
1704
|
+
"""
|
|
1705
|
+
return {
|
|
1706
|
+
"agent_id": self.agent_id,
|
|
1707
|
+
"name": self.name,
|
|
1708
|
+
"type": self.agent_type.value,
|
|
1709
|
+
"version": self.version,
|
|
1710
|
+
"state": self._state.value,
|
|
1711
|
+
"health": self.get_health_status(),
|
|
1712
|
+
"performance": self.get_performance_metrics(),
|
|
1713
|
+
"metrics": {
|
|
1714
|
+
# Task metrics
|
|
1715
|
+
"total_tasks_executed": self._metrics.total_tasks_executed,
|
|
1716
|
+
"successful_tasks": self._metrics.successful_tasks,
|
|
1717
|
+
"failed_tasks": self._metrics.failed_tasks,
|
|
1718
|
+
"success_rate": self._metrics.success_rate,
|
|
1719
|
+
# Execution time metrics
|
|
1720
|
+
"average_execution_time": self._metrics.average_execution_time,
|
|
1721
|
+
"total_execution_time": self._metrics.total_execution_time,
|
|
1722
|
+
# Session metrics
|
|
1723
|
+
"total_sessions": self._metrics.total_sessions,
|
|
1724
|
+
"active_sessions": self._metrics.active_sessions,
|
|
1725
|
+
"completed_sessions": self._metrics.completed_sessions,
|
|
1726
|
+
"failed_sessions": self._metrics.failed_sessions,
|
|
1727
|
+
"expired_sessions": self._metrics.expired_sessions,
|
|
1728
|
+
# Resource usage
|
|
1729
|
+
"total_tokens_used": self._metrics.total_tokens_used,
|
|
1730
|
+
"total_tool_calls": self._metrics.total_tool_calls,
|
|
1731
|
+
# Error tracking
|
|
1732
|
+
"error_count": self._metrics.error_count,
|
|
1733
|
+
"error_types": self._metrics.error_types,
|
|
1734
|
+
# Prompt cache metrics
|
|
1735
|
+
"cache_hit_rate": self._metrics.cache_hit_rate,
|
|
1736
|
+
"cache_hits": self._metrics.cache_hits,
|
|
1737
|
+
"cache_misses": self._metrics.cache_misses,
|
|
1738
|
+
"total_cache_read_tokens": self._metrics.total_cache_read_tokens,
|
|
1739
|
+
"estimated_cache_savings_tokens": self._metrics.estimated_cache_savings_tokens,
|
|
1740
|
+
},
|
|
1741
|
+
"capabilities": [cap.capability_type for cap in self.get_capabilities()],
|
|
1742
|
+
"active_goals": len([g for g in self._goals.values() if g.status == GoalStatus.IN_PROGRESS]),
|
|
1743
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
def reset_metrics(self) -> None:
|
|
1747
|
+
"""
|
|
1748
|
+
Reset performance and session metrics.
|
|
1749
|
+
|
|
1750
|
+
Resets all metrics to their initial state while preserving
|
|
1751
|
+
agent configuration and state.
|
|
1752
|
+
|
|
1753
|
+
Example:
|
|
1754
|
+
# Reset metrics at the start of a new monitoring period
|
|
1755
|
+
agent.reset_metrics()
|
|
1756
|
+
"""
|
|
1757
|
+
self._metrics = AgentMetrics(last_reset_at=datetime.utcnow()) # type: ignore[call-arg]
|
|
1758
|
+
logger.info(f"Agent {self.agent_id} metrics reset")
|
|
1759
|
+
|
|
1760
|
+
# ==================== Serialization ====================
|
|
1761
|
+
|
|
1762
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
1763
|
+
"""
|
|
1764
|
+
Serialize agent to dictionary.
|
|
1765
|
+
|
|
1766
|
+
Includes health status and performance metrics for comprehensive
|
|
1767
|
+
agent state representation.
|
|
1768
|
+
|
|
1769
|
+
Returns:
|
|
1770
|
+
Dictionary representation
|
|
1771
|
+
|
|
1772
|
+
Raises:
|
|
1773
|
+
SerializationError: If serialization fails
|
|
1774
|
+
"""
|
|
1775
|
+
try:
|
|
1776
|
+
return {
|
|
1777
|
+
"agent_id": self.agent_id,
|
|
1778
|
+
"name": self.name,
|
|
1779
|
+
"agent_type": self.agent_type.value,
|
|
1780
|
+
"description": self.description,
|
|
1781
|
+
"version": self.version,
|
|
1782
|
+
"state": self._state.value,
|
|
1783
|
+
"config": self._config.model_dump(),
|
|
1784
|
+
"goals": [g.model_dump() for g in self._goals.values()],
|
|
1785
|
+
"capabilities": [c.model_dump() for c in self._capabilities.values()],
|
|
1786
|
+
"metrics": self._metrics.model_dump(),
|
|
1787
|
+
"health_status": self.get_health_status(), # Phase 3 enhancement
|
|
1788
|
+
"performance_metrics": self.get_performance_metrics(), # Phase 3 enhancement
|
|
1789
|
+
"memory_summary": self.get_memory_summary(),
|
|
1790
|
+
"created_at": self.created_at.isoformat(),
|
|
1791
|
+
"updated_at": self.updated_at.isoformat(),
|
|
1792
|
+
"last_active_at": (self.last_active_at.isoformat() if self.last_active_at else None),
|
|
1793
|
+
}
|
|
1794
|
+
except Exception as e:
|
|
1795
|
+
raise SerializationError(
|
|
1796
|
+
f"Failed to serialize agent: {str(e)}",
|
|
1797
|
+
agent_id=self.agent_id,
|
|
1798
|
+
)
|
|
1799
|
+
|
|
1800
|
+
@classmethod
|
|
1801
|
+
def from_dict(cls, data: Dict[str, Any]) -> "BaseAIAgent":
|
|
1802
|
+
"""
|
|
1803
|
+
Deserialize agent from dictionary.
|
|
1804
|
+
|
|
1805
|
+
Args:
|
|
1806
|
+
data: Dictionary representation
|
|
1807
|
+
|
|
1808
|
+
Returns:
|
|
1809
|
+
Agent instance
|
|
1810
|
+
|
|
1811
|
+
Raises:
|
|
1812
|
+
SerializationError: If deserialization fails
|
|
1813
|
+
"""
|
|
1814
|
+
raise NotImplementedError("from_dict must be implemented by subclasses")
|
|
1815
|
+
|
|
1816
|
+
# ==================== Checkpointer Support ====================
|
|
1817
|
+
|
|
1818
|
+
async def save_checkpoint(self, session_id: str, checkpoint_id: Optional[str] = None) -> Optional[str]:
|
|
1819
|
+
"""
|
|
1820
|
+
Save agent state checkpoint.
|
|
1821
|
+
|
|
1822
|
+
This method saves the current agent state using the configured checkpointer.
|
|
1823
|
+
If no checkpointer is configured, logs a warning and returns None.
|
|
1824
|
+
|
|
1825
|
+
Args:
|
|
1826
|
+
session_id: Session identifier for the checkpoint
|
|
1827
|
+
checkpoint_id: Optional checkpoint identifier (auto-generated if None)
|
|
1828
|
+
|
|
1829
|
+
Returns:
|
|
1830
|
+
Checkpoint ID if saved successfully, None otherwise
|
|
1831
|
+
|
|
1832
|
+
Example:
|
|
1833
|
+
# Save checkpoint with auto-generated ID
|
|
1834
|
+
checkpoint_id = await agent.save_checkpoint(session_id="session-123")
|
|
1835
|
+
|
|
1836
|
+
# Save checkpoint with custom ID
|
|
1837
|
+
checkpoint_id = await agent.save_checkpoint(
|
|
1838
|
+
session_id="session-123",
|
|
1839
|
+
checkpoint_id="v1.0"
|
|
1840
|
+
)
|
|
1841
|
+
|
|
1842
|
+
Note:
|
|
1843
|
+
Requires a checkpointer to be configured during agent initialization.
|
|
1844
|
+
The checkpoint includes full agent state from to_dict().
|
|
1845
|
+
"""
|
|
1846
|
+
if not self._checkpointer:
|
|
1847
|
+
logger.warning(f"Agent {self.agent_id}: No checkpointer configured, cannot save checkpoint")
|
|
1848
|
+
return None
|
|
1849
|
+
|
|
1850
|
+
try:
|
|
1851
|
+
# Get current agent state
|
|
1852
|
+
checkpoint_data = self.to_dict()
|
|
1853
|
+
|
|
1854
|
+
# Add checkpoint metadata
|
|
1855
|
+
checkpoint_data["checkpoint_metadata"] = {
|
|
1856
|
+
"session_id": session_id,
|
|
1857
|
+
"checkpoint_id": checkpoint_id,
|
|
1858
|
+
"saved_at": datetime.utcnow().isoformat(),
|
|
1859
|
+
"agent_version": self.version,
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
# Save using checkpointer
|
|
1863
|
+
saved_checkpoint_id = await self._checkpointer.save_checkpoint(
|
|
1864
|
+
agent_id=self.agent_id,
|
|
1865
|
+
session_id=session_id,
|
|
1866
|
+
checkpoint_data=checkpoint_data,
|
|
1867
|
+
)
|
|
1868
|
+
|
|
1869
|
+
logger.info(f"Agent {self.agent_id}: Checkpoint saved successfully " f"(session={session_id}, checkpoint={saved_checkpoint_id})")
|
|
1870
|
+
return saved_checkpoint_id
|
|
1871
|
+
|
|
1872
|
+
except Exception as e:
|
|
1873
|
+
logger.error(f"Agent {self.agent_id}: Failed to save checkpoint " f"(session={session_id}): {e}")
|
|
1874
|
+
return None
|
|
1875
|
+
|
|
1876
|
+
async def load_checkpoint(self, session_id: str, checkpoint_id: Optional[str] = None) -> bool:
|
|
1877
|
+
"""
|
|
1878
|
+
Load agent state from checkpoint.
|
|
1879
|
+
|
|
1880
|
+
This method loads agent state from a saved checkpoint using the configured
|
|
1881
|
+
checkpointer. If no checkpointer is configured, logs a warning and returns False.
|
|
1882
|
+
|
|
1883
|
+
Args:
|
|
1884
|
+
session_id: Session identifier for the checkpoint
|
|
1885
|
+
checkpoint_id: Optional checkpoint identifier (loads latest if None)
|
|
1886
|
+
|
|
1887
|
+
Returns:
|
|
1888
|
+
True if checkpoint loaded successfully, False otherwise
|
|
1889
|
+
|
|
1890
|
+
Example:
|
|
1891
|
+
# Load latest checkpoint
|
|
1892
|
+
success = await agent.load_checkpoint(session_id="session-123")
|
|
1893
|
+
|
|
1894
|
+
# Load specific checkpoint
|
|
1895
|
+
success = await agent.load_checkpoint(
|
|
1896
|
+
session_id="session-123",
|
|
1897
|
+
checkpoint_id="v1.0"
|
|
1898
|
+
)
|
|
1899
|
+
|
|
1900
|
+
Note:
|
|
1901
|
+
Requires a checkpointer to be configured during agent initialization.
|
|
1902
|
+
This method updates the agent's internal state from the checkpoint.
|
|
1903
|
+
Not all state may be restorable (e.g., runtime objects, connections).
|
|
1904
|
+
"""
|
|
1905
|
+
if not self._checkpointer:
|
|
1906
|
+
logger.warning(f"Agent {self.agent_id}: No checkpointer configured, cannot load checkpoint")
|
|
1907
|
+
return False
|
|
1908
|
+
|
|
1909
|
+
try:
|
|
1910
|
+
# Load checkpoint data
|
|
1911
|
+
checkpoint_data = await self._checkpointer.load_checkpoint(
|
|
1912
|
+
agent_id=self.agent_id,
|
|
1913
|
+
session_id=session_id,
|
|
1914
|
+
checkpoint_id=checkpoint_id,
|
|
1915
|
+
)
|
|
1916
|
+
|
|
1917
|
+
if not checkpoint_data:
|
|
1918
|
+
logger.warning(f"Agent {self.agent_id}: No checkpoint found " f"(session={session_id}, checkpoint={checkpoint_id or 'latest'})")
|
|
1919
|
+
return False
|
|
1920
|
+
|
|
1921
|
+
# Restore agent state from checkpoint
|
|
1922
|
+
self._restore_from_checkpoint(checkpoint_data)
|
|
1923
|
+
|
|
1924
|
+
logger.info(f"Agent {self.agent_id}: Checkpoint loaded successfully " f"(session={session_id}, checkpoint={checkpoint_id or 'latest'})")
|
|
1925
|
+
return True
|
|
1926
|
+
|
|
1927
|
+
except Exception as e:
|
|
1928
|
+
logger.error(f"Agent {self.agent_id}: Failed to load checkpoint " f"(session={session_id}, checkpoint={checkpoint_id or 'latest'}): {e}")
|
|
1929
|
+
return False
|
|
1930
|
+
|
|
1931
|
+
def _restore_from_checkpoint(self, checkpoint_data: Dict[str, Any]) -> None:
|
|
1932
|
+
"""
|
|
1933
|
+
Restore agent state from checkpoint data.
|
|
1934
|
+
|
|
1935
|
+
This is an internal method that updates the agent's state from checkpoint data.
|
|
1936
|
+
Subclasses can override this to customize restoration logic.
|
|
1937
|
+
|
|
1938
|
+
Args:
|
|
1939
|
+
checkpoint_data: Checkpoint data dictionary
|
|
1940
|
+
|
|
1941
|
+
Note:
|
|
1942
|
+
This method restores basic agent state. Runtime objects like
|
|
1943
|
+
connections, file handles, etc. are not restored.
|
|
1944
|
+
"""
|
|
1945
|
+
# Restore basic state
|
|
1946
|
+
if "state" in checkpoint_data:
|
|
1947
|
+
try:
|
|
1948
|
+
self._state = AgentState(checkpoint_data["state"])
|
|
1949
|
+
except (ValueError, KeyError):
|
|
1950
|
+
logger.warning("Could not restore state from checkpoint")
|
|
1951
|
+
|
|
1952
|
+
# Restore metrics
|
|
1953
|
+
if "metrics" in checkpoint_data:
|
|
1954
|
+
try:
|
|
1955
|
+
self._metrics = AgentMetrics(**checkpoint_data["metrics"])
|
|
1956
|
+
except Exception as e:
|
|
1957
|
+
logger.warning(f"Could not restore metrics from checkpoint: {e}")
|
|
1958
|
+
|
|
1959
|
+
# Restore goals
|
|
1960
|
+
if "goals" in checkpoint_data:
|
|
1961
|
+
try:
|
|
1962
|
+
self._goals = {}
|
|
1963
|
+
for goal_data in checkpoint_data["goals"]:
|
|
1964
|
+
goal = AgentGoal(**goal_data)
|
|
1965
|
+
self._goals[goal.goal_id] = goal
|
|
1966
|
+
except Exception as e:
|
|
1967
|
+
logger.warning(f"Could not restore goals from checkpoint: {e}")
|
|
1968
|
+
|
|
1969
|
+
# Update timestamps
|
|
1970
|
+
self.updated_at = datetime.utcnow()
|
|
1971
|
+
|
|
1972
|
+
logger.debug(f"Agent {self.agent_id}: State restored from checkpoint")
|
|
1973
|
+
|
|
1974
|
+
# ==================== Utility Methods ====================
|
|
1975
|
+
|
|
1976
|
+
def is_available(self) -> bool:
|
|
1977
|
+
"""Check if agent is available for tasks."""
|
|
1978
|
+
return self._state == AgentState.ACTIVE
|
|
1979
|
+
|
|
1980
|
+
def is_busy(self) -> bool:
|
|
1981
|
+
"""Check if agent is currently busy."""
|
|
1982
|
+
return self._state == AgentState.BUSY
|
|
1983
|
+
|
|
1984
|
+
async def execute_tool(self, tool_name: str, parameters: Dict[str, Any]) -> Any:
|
|
1985
|
+
"""
|
|
1986
|
+
Execute a single tool with given parameters.
|
|
1987
|
+
|
|
1988
|
+
This is a default implementation that subclasses can override.
|
|
1989
|
+
For ToolAgent, this calls _execute_tool with operation from parameters.
|
|
1990
|
+
|
|
1991
|
+
Args:
|
|
1992
|
+
tool_name: Name of the tool to execute
|
|
1993
|
+
parameters: Tool parameters (may include 'operation' key)
|
|
1994
|
+
|
|
1995
|
+
Returns:
|
|
1996
|
+
Tool execution result
|
|
1997
|
+
"""
|
|
1998
|
+
# Check if we have tool instances
|
|
1999
|
+
if hasattr(self, "_tool_instances") and self._tool_instances:
|
|
2000
|
+
tool = self._tool_instances.get(tool_name)
|
|
2001
|
+
if tool:
|
|
2002
|
+
# Make a copy to avoid modifying the original
|
|
2003
|
+
params = parameters.copy()
|
|
2004
|
+
|
|
2005
|
+
# Try to execute the tool directly (for custom tools with execute method)
|
|
2006
|
+
if hasattr(tool, "execute"):
|
|
2007
|
+
return await tool.execute(**params)
|
|
2008
|
+
# For standard tools with run_async
|
|
2009
|
+
elif hasattr(tool, "run_async"):
|
|
2010
|
+
# Check if operation is specified
|
|
2011
|
+
operation = params.pop("operation", None)
|
|
2012
|
+
if operation:
|
|
2013
|
+
return await tool.run_async(operation, **params)
|
|
2014
|
+
else:
|
|
2015
|
+
return await tool.run_async(**params)
|
|
2016
|
+
|
|
2017
|
+
raise NotImplementedError(f"execute_tool not implemented for {self.__class__.__name__}. " "Tool {tool_name} not found or doesn't have execute/run_async method.")
|
|
2018
|
+
|
|
2019
|
+
# ==================== Parallel Tool Execution (Phase 7) ====================
|
|
2020
|
+
|
|
2021
|
+
async def execute_tools_parallel(
|
|
2022
|
+
self,
|
|
2023
|
+
tool_calls: List[Dict[str, Any]],
|
|
2024
|
+
max_concurrency: int = 5,
|
|
2025
|
+
) -> List[Dict[str, Any]]:
|
|
2026
|
+
"""
|
|
2027
|
+
Execute multiple tools in parallel with concurrency limit.
|
|
2028
|
+
|
|
2029
|
+
Args:
|
|
2030
|
+
tool_calls: List of tool call dicts with 'tool_name' and 'parameters'
|
|
2031
|
+
max_concurrency: Maximum number of concurrent tool executions
|
|
2032
|
+
|
|
2033
|
+
Returns:
|
|
2034
|
+
List of results in same order as tool_calls
|
|
2035
|
+
|
|
2036
|
+
Example:
|
|
2037
|
+
tool_calls = [
|
|
2038
|
+
{"tool_name": "search", "parameters": {"query": "AI"}},
|
|
2039
|
+
{"tool_name": "calculator", "parameters": {"expression": "2+2"}},
|
|
2040
|
+
{"tool_name": "search", "parameters": {"query": "ML"}},
|
|
2041
|
+
]
|
|
2042
|
+
results = await agent.execute_tools_parallel(tool_calls, max_concurrency=2)
|
|
2043
|
+
"""
|
|
2044
|
+
if not tool_calls:
|
|
2045
|
+
return []
|
|
2046
|
+
|
|
2047
|
+
# Create semaphore for concurrency control
|
|
2048
|
+
semaphore = asyncio.Semaphore(max_concurrency)
|
|
2049
|
+
|
|
2050
|
+
async def execute_with_semaphore(tool_call: Dict[str, Any], index: int):
|
|
2051
|
+
"""Execute tool with semaphore."""
|
|
2052
|
+
async with semaphore:
|
|
2053
|
+
tool_name = tool_call.get("tool_name")
|
|
2054
|
+
parameters = tool_call.get("parameters", {})
|
|
2055
|
+
|
|
2056
|
+
if tool_name is None:
|
|
2057
|
+
raise ValueError("tool_name is required in tool_call")
|
|
2058
|
+
|
|
2059
|
+
try:
|
|
2060
|
+
# Execute tool (subclass should implement execute_tool)
|
|
2061
|
+
result = await self.execute_tool(tool_name, parameters)
|
|
2062
|
+
return {"index": index, "success": True, "result": result}
|
|
2063
|
+
except Exception as e:
|
|
2064
|
+
logger.error(f"Tool {tool_name} failed: {e}")
|
|
2065
|
+
return {
|
|
2066
|
+
"index": index,
|
|
2067
|
+
"success": False,
|
|
2068
|
+
"error": str(e),
|
|
2069
|
+
"tool_name": tool_name,
|
|
2070
|
+
}
|
|
2071
|
+
|
|
2072
|
+
# Execute all tools in parallel
|
|
2073
|
+
tasks = [execute_with_semaphore(tool_call, i) for i, tool_call in enumerate(tool_calls)]
|
|
2074
|
+
|
|
2075
|
+
results_unordered = await asyncio.gather(*tasks, return_exceptions=True)
|
|
2076
|
+
|
|
2077
|
+
# Sort results by index to maintain order
|
|
2078
|
+
valid_results = [r for r in results_unordered if not isinstance(r, Exception) and isinstance(r, dict) and "index" in r]
|
|
2079
|
+
results_sorted = sorted(
|
|
2080
|
+
valid_results,
|
|
2081
|
+
key=lambda x: x["index"], # type: ignore[index]
|
|
2082
|
+
)
|
|
2083
|
+
|
|
2084
|
+
# Remove index from results
|
|
2085
|
+
return [{k: v for k, v in r.items() if k != "index"} for r in results_sorted]
|
|
2086
|
+
|
|
2087
|
+
async def analyze_tool_dependencies(self, tool_calls: List[Dict[str, Any]]) -> Dict[str, List[str]]:
|
|
2088
|
+
"""
|
|
2089
|
+
Analyze dependencies between tool calls.
|
|
2090
|
+
|
|
2091
|
+
Detects if one tool's output is used as input to another tool.
|
|
2092
|
+
|
|
2093
|
+
Args:
|
|
2094
|
+
tool_calls: List of tool call dicts
|
|
2095
|
+
|
|
2096
|
+
Returns:
|
|
2097
|
+
Dict mapping tool index to list of dependency indices
|
|
2098
|
+
|
|
2099
|
+
Example:
|
|
2100
|
+
tool_calls = [
|
|
2101
|
+
{"tool_name": "search", "parameters": {"query": "AI"}},
|
|
2102
|
+
{"tool_name": "summarize", "parameters": {"text": "${0.result}"}},
|
|
2103
|
+
]
|
|
2104
|
+
deps = await agent.analyze_tool_dependencies(tool_calls)
|
|
2105
|
+
# deps = {"1": ["0"]} # Tool 1 depends on tool 0
|
|
2106
|
+
"""
|
|
2107
|
+
dependencies: Dict[str, List[str]] = {}
|
|
2108
|
+
|
|
2109
|
+
for i, tool_call in enumerate(tool_calls):
|
|
2110
|
+
deps = []
|
|
2111
|
+
parameters = tool_call.get("parameters", {})
|
|
2112
|
+
|
|
2113
|
+
# Check if parameters reference other tool results
|
|
2114
|
+
param_str = json.dumps(parameters)
|
|
2115
|
+
|
|
2116
|
+
# Look for ${index.field} patterns
|
|
2117
|
+
import re
|
|
2118
|
+
|
|
2119
|
+
matches = re.findall(r"\$\{(\d+)\.", param_str)
|
|
2120
|
+
deps = list(set(matches)) # Remove duplicates
|
|
2121
|
+
|
|
2122
|
+
if deps:
|
|
2123
|
+
dependencies[str(i)] = deps
|
|
2124
|
+
|
|
2125
|
+
return dependencies
|
|
2126
|
+
|
|
2127
|
+
async def execute_tools_with_dependencies(self, tool_calls: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
|
2128
|
+
"""
|
|
2129
|
+
Execute tools respecting dependencies using topological sort.
|
|
2130
|
+
|
|
2131
|
+
Args:
|
|
2132
|
+
tool_calls: List of tool call dicts
|
|
2133
|
+
|
|
2134
|
+
Returns:
|
|
2135
|
+
List of results in same order as tool_calls
|
|
2136
|
+
|
|
2137
|
+
Example:
|
|
2138
|
+
tool_calls = [
|
|
2139
|
+
{"tool_name": "search", "parameters": {"query": "AI"}},
|
|
2140
|
+
{"tool_name": "summarize", "parameters": {"text": "${0.result}"}},
|
|
2141
|
+
]
|
|
2142
|
+
results = await agent.execute_tools_with_dependencies(tool_calls)
|
|
2143
|
+
"""
|
|
2144
|
+
# Analyze dependencies
|
|
2145
|
+
dependencies = await self.analyze_tool_dependencies(tool_calls)
|
|
2146
|
+
|
|
2147
|
+
# Topological sort
|
|
2148
|
+
executed: Set[int] = set()
|
|
2149
|
+
results: List[Optional[Dict[str, Any]]] = [None] * len(tool_calls)
|
|
2150
|
+
|
|
2151
|
+
def can_execute(index: int) -> bool:
|
|
2152
|
+
"""Check if tool can be executed."""
|
|
2153
|
+
deps = dependencies.get(str(index), [])
|
|
2154
|
+
return all(int(dep) in executed for dep in deps)
|
|
2155
|
+
|
|
2156
|
+
# Execute tools in dependency order
|
|
2157
|
+
while len(executed) < len(tool_calls):
|
|
2158
|
+
# Find tools that can be executed
|
|
2159
|
+
ready = [i for i in range(len(tool_calls)) if i not in executed and can_execute(i)]
|
|
2160
|
+
|
|
2161
|
+
if not ready:
|
|
2162
|
+
# Circular dependency or error
|
|
2163
|
+
logger.error("Circular dependency detected or no tools ready")
|
|
2164
|
+
break
|
|
2165
|
+
|
|
2166
|
+
# Execute ready tools in parallel
|
|
2167
|
+
ready_calls = [tool_calls[i] for i in ready]
|
|
2168
|
+
ready_results = await self.execute_tools_parallel(ready_calls)
|
|
2169
|
+
|
|
2170
|
+
# Store results and mark as executed
|
|
2171
|
+
for i, result in zip(ready, ready_results):
|
|
2172
|
+
if result is not None:
|
|
2173
|
+
results[i] = result
|
|
2174
|
+
executed.add(i)
|
|
2175
|
+
|
|
2176
|
+
# Substitute results in dependent tool calls
|
|
2177
|
+
for j in range(len(tool_calls)):
|
|
2178
|
+
if j not in executed:
|
|
2179
|
+
tool_calls[j] = self._substitute_tool_result(tool_calls[j], i, result)
|
|
2180
|
+
|
|
2181
|
+
# Filter out None values and return
|
|
2182
|
+
return [r for r in results if r is not None]
|
|
2183
|
+
|
|
2184
|
+
def _substitute_tool_result(self, tool_call: Dict[str, Any], source_index: int, source_result: Dict[str, Any]) -> Dict[str, Any]:
|
|
2185
|
+
"""
|
|
2186
|
+
Substitute tool result references in parameters.
|
|
2187
|
+
|
|
2188
|
+
Args:
|
|
2189
|
+
tool_call: Tool call dict
|
|
2190
|
+
source_index: Index of source tool
|
|
2191
|
+
source_result: Result from source tool
|
|
2192
|
+
|
|
2193
|
+
Returns:
|
|
2194
|
+
Updated tool call dict
|
|
2195
|
+
"""
|
|
2196
|
+
import re
|
|
2197
|
+
|
|
2198
|
+
param_str = json.dumps(tool_call.get("parameters", {}))
|
|
2199
|
+
|
|
2200
|
+
# Replace ${index.field} with actual values
|
|
2201
|
+
pattern = rf"\$\{{{source_index}\.(\w+)\}}"
|
|
2202
|
+
|
|
2203
|
+
def replacer(match):
|
|
2204
|
+
field = match.group(1)
|
|
2205
|
+
value = source_result.get(field)
|
|
2206
|
+
return json.dumps(value) if value is not None else "null"
|
|
2207
|
+
|
|
2208
|
+
param_str = re.sub(pattern, replacer, param_str)
|
|
2209
|
+
|
|
2210
|
+
tool_call["parameters"] = json.loads(param_str)
|
|
2211
|
+
return tool_call
|
|
2212
|
+
|
|
2213
|
+
# ==================== Tool Result Caching (Phase 7) ====================
|
|
2214
|
+
|
|
2215
|
+
def _generate_cache_key(self, tool_name: str, parameters: Dict[str, Any]) -> str:
|
|
2216
|
+
"""
|
|
2217
|
+
Generate cache key for tool result.
|
|
2218
|
+
|
|
2219
|
+
Args:
|
|
2220
|
+
tool_name: Name of the tool
|
|
2221
|
+
parameters: Tool parameters
|
|
2222
|
+
|
|
2223
|
+
Returns:
|
|
2224
|
+
Cache key string
|
|
2225
|
+
|
|
2226
|
+
Example:
|
|
2227
|
+
key = agent._generate_cache_key("search", {"query": "AI"})
|
|
2228
|
+
"""
|
|
2229
|
+
# Sort parameters for consistent keys
|
|
2230
|
+
param_str = json.dumps(parameters, sort_keys=True)
|
|
2231
|
+
|
|
2232
|
+
# Hash large inputs
|
|
2233
|
+
if self._cache_config.hash_large_inputs and len(param_str) > 1024:
|
|
2234
|
+
import hashlib
|
|
2235
|
+
|
|
2236
|
+
param_hash = hashlib.md5(param_str.encode()).hexdigest()
|
|
2237
|
+
cache_key = f"{tool_name}:{param_hash}"
|
|
2238
|
+
else:
|
|
2239
|
+
cache_key = f"{tool_name}:{param_str}"
|
|
2240
|
+
|
|
2241
|
+
# Include timestamp if configured
|
|
2242
|
+
if self._cache_config.include_timestamp_in_key:
|
|
2243
|
+
timestamp = int(time.time() / 60) # Minute-level granularity
|
|
2244
|
+
cache_key = f"{cache_key}:{timestamp}"
|
|
2245
|
+
|
|
2246
|
+
return cache_key
|
|
2247
|
+
|
|
2248
|
+
async def execute_tool_with_cache(self, tool_name: str, parameters: Dict[str, Any]) -> Any:
|
|
2249
|
+
"""
|
|
2250
|
+
Execute tool with caching support.
|
|
2251
|
+
|
|
2252
|
+
Args:
|
|
2253
|
+
tool_name: Name of the tool
|
|
2254
|
+
parameters: Tool parameters
|
|
2255
|
+
|
|
2256
|
+
Returns:
|
|
2257
|
+
Tool result (from cache or fresh execution)
|
|
2258
|
+
|
|
2259
|
+
Example:
|
|
2260
|
+
result = await agent.execute_tool_with_cache("search", {"query": "AI"})
|
|
2261
|
+
"""
|
|
2262
|
+
if not self._cache_config.enabled:
|
|
2263
|
+
# Cache disabled, execute directly
|
|
2264
|
+
return await self.execute_tool(tool_name, parameters)
|
|
2265
|
+
|
|
2266
|
+
# Generate cache key
|
|
2267
|
+
cache_key = self._generate_cache_key(tool_name, parameters)
|
|
2268
|
+
|
|
2269
|
+
# Check cache
|
|
2270
|
+
if cache_key in self._tool_cache:
|
|
2271
|
+
# Check TTL
|
|
2272
|
+
cached_time = self._cache_timestamps.get(cache_key, 0)
|
|
2273
|
+
ttl = self._cache_config.get_ttl(tool_name)
|
|
2274
|
+
age = time.time() - cached_time
|
|
2275
|
+
|
|
2276
|
+
if age < ttl:
|
|
2277
|
+
# Cache hit
|
|
2278
|
+
self._cache_access_count[cache_key] = self._cache_access_count.get(cache_key, 0) + 1
|
|
2279
|
+
logger.debug(f"Cache hit for {tool_name} (age: {age:.1f}s)")
|
|
2280
|
+
return self._tool_cache[cache_key]
|
|
2281
|
+
else:
|
|
2282
|
+
# Cache expired
|
|
2283
|
+
logger.debug(f"Cache expired for {tool_name} (age: {age:.1f}s)")
|
|
2284
|
+
del self._tool_cache[cache_key]
|
|
2285
|
+
del self._cache_timestamps[cache_key]
|
|
2286
|
+
if cache_key in self._cache_access_count:
|
|
2287
|
+
del self._cache_access_count[cache_key]
|
|
2288
|
+
|
|
2289
|
+
# Cache miss - execute tool
|
|
2290
|
+
logger.debug(f"Cache miss for {tool_name}")
|
|
2291
|
+
result = await self.execute_tool(tool_name, parameters)
|
|
2292
|
+
|
|
2293
|
+
# Store in cache
|
|
2294
|
+
self._tool_cache[cache_key] = result
|
|
2295
|
+
self._cache_timestamps[cache_key] = time.time()
|
|
2296
|
+
self._cache_access_count[cache_key] = 0
|
|
2297
|
+
|
|
2298
|
+
# Cleanup if needed
|
|
2299
|
+
await self._cleanup_cache()
|
|
2300
|
+
|
|
2301
|
+
return result
|
|
2302
|
+
|
|
2303
|
+
def invalidate_cache(self, tool_name: Optional[str] = None, pattern: Optional[str] = None) -> int:
|
|
2304
|
+
"""
|
|
2305
|
+
Invalidate cache entries.
|
|
2306
|
+
|
|
2307
|
+
Args:
|
|
2308
|
+
tool_name: Invalidate all entries for this tool (optional)
|
|
2309
|
+
pattern: Invalidate entries matching pattern (optional)
|
|
2310
|
+
|
|
2311
|
+
Returns:
|
|
2312
|
+
Number of entries invalidated
|
|
2313
|
+
|
|
2314
|
+
Example:
|
|
2315
|
+
# Invalidate all search results
|
|
2316
|
+
count = agent.invalidate_cache(tool_name="search")
|
|
2317
|
+
|
|
2318
|
+
# Invalidate all cache
|
|
2319
|
+
count = agent.invalidate_cache()
|
|
2320
|
+
"""
|
|
2321
|
+
if tool_name is None and pattern is None:
|
|
2322
|
+
# Invalidate all
|
|
2323
|
+
count = len(self._tool_cache)
|
|
2324
|
+
self._tool_cache.clear()
|
|
2325
|
+
self._cache_timestamps.clear()
|
|
2326
|
+
self._cache_access_count.clear()
|
|
2327
|
+
logger.info(f"Invalidated all cache ({count} entries)")
|
|
2328
|
+
return count
|
|
2329
|
+
|
|
2330
|
+
# Invalidate matching entries
|
|
2331
|
+
keys_to_delete = []
|
|
2332
|
+
|
|
2333
|
+
for key in list(self._tool_cache.keys()):
|
|
2334
|
+
if tool_name and key.startswith(f"{tool_name}:"):
|
|
2335
|
+
keys_to_delete.append(key)
|
|
2336
|
+
elif pattern and pattern in key:
|
|
2337
|
+
keys_to_delete.append(key)
|
|
2338
|
+
|
|
2339
|
+
for key in keys_to_delete:
|
|
2340
|
+
del self._tool_cache[key]
|
|
2341
|
+
del self._cache_timestamps[key]
|
|
2342
|
+
if key in self._cache_access_count:
|
|
2343
|
+
del self._cache_access_count[key]
|
|
2344
|
+
|
|
2345
|
+
logger.info(f"Invalidated {len(keys_to_delete)} cache entries")
|
|
2346
|
+
return len(keys_to_delete)
|
|
2347
|
+
|
|
2348
|
+
def get_cache_stats(self) -> Dict[str, Any]:
|
|
2349
|
+
"""
|
|
2350
|
+
Get cache statistics.
|
|
2351
|
+
|
|
2352
|
+
Returns:
|
|
2353
|
+
Dictionary with cache statistics
|
|
2354
|
+
|
|
2355
|
+
Example:
|
|
2356
|
+
stats = agent.get_cache_stats()
|
|
2357
|
+
print(f"Cache size: {stats['size']}")
|
|
2358
|
+
print(f"Hit rate: {stats['hit_rate']:.1%}")
|
|
2359
|
+
"""
|
|
2360
|
+
total_entries = len(self._tool_cache)
|
|
2361
|
+
total_accesses = sum(self._cache_access_count.values())
|
|
2362
|
+
|
|
2363
|
+
# Calculate hit rate (approximate)
|
|
2364
|
+
cache_hits = sum(count for count in self._cache_access_count.values() if count > 0)
|
|
2365
|
+
hit_rate = cache_hits / total_accesses if total_accesses > 0 else 0.0
|
|
2366
|
+
|
|
2367
|
+
# Calculate memory usage (approximate)
|
|
2368
|
+
import sys
|
|
2369
|
+
|
|
2370
|
+
memory_bytes = sum(sys.getsizeof(v) for v in self._tool_cache.values())
|
|
2371
|
+
memory_mb = memory_bytes / (1024 * 1024)
|
|
2372
|
+
|
|
2373
|
+
# Per-tool stats
|
|
2374
|
+
tool_stats = {}
|
|
2375
|
+
for key in self._tool_cache.keys():
|
|
2376
|
+
tool_name = key.split(":")[0]
|
|
2377
|
+
if tool_name not in tool_stats:
|
|
2378
|
+
tool_stats[tool_name] = {"count": 0, "accesses": 0}
|
|
2379
|
+
tool_stats[tool_name]["count"] += 1
|
|
2380
|
+
tool_stats[tool_name]["accesses"] += self._cache_access_count.get(key, 0)
|
|
2381
|
+
|
|
2382
|
+
return {
|
|
2383
|
+
"enabled": self._cache_config.enabled,
|
|
2384
|
+
"size": total_entries,
|
|
2385
|
+
"max_size": self._cache_config.max_cache_size,
|
|
2386
|
+
"memory_mb": memory_mb,
|
|
2387
|
+
"max_memory_mb": self._cache_config.max_memory_mb,
|
|
2388
|
+
"total_accesses": total_accesses,
|
|
2389
|
+
"hit_rate": hit_rate,
|
|
2390
|
+
"tool_stats": tool_stats,
|
|
2391
|
+
}
|
|
2392
|
+
|
|
2393
|
+
async def _cleanup_cache(self) -> None:
|
|
2394
|
+
"""
|
|
2395
|
+
Cleanup cache based on size and memory limits.
|
|
2396
|
+
|
|
2397
|
+
Removes least recently used entries when limits are exceeded.
|
|
2398
|
+
"""
|
|
2399
|
+
# Check if cleanup needed
|
|
2400
|
+
current_time = time.time()
|
|
2401
|
+
if current_time - self._last_cleanup_time < self._cache_config.cleanup_interval:
|
|
2402
|
+
return
|
|
2403
|
+
|
|
2404
|
+
self._last_cleanup_time = current_time
|
|
2405
|
+
|
|
2406
|
+
# Check size limit
|
|
2407
|
+
if len(self._tool_cache) > self._cache_config.max_cache_size * self._cache_config.cleanup_threshold:
|
|
2408
|
+
# Remove oldest entries
|
|
2409
|
+
entries_to_remove = int(len(self._tool_cache) - self._cache_config.max_cache_size * 0.8)
|
|
2410
|
+
|
|
2411
|
+
# Sort by timestamp (oldest first)
|
|
2412
|
+
sorted_keys = sorted(self._cache_timestamps.items(), key=lambda x: x[1])
|
|
2413
|
+
|
|
2414
|
+
for key, _ in sorted_keys[:entries_to_remove]:
|
|
2415
|
+
del self._tool_cache[key]
|
|
2416
|
+
del self._cache_timestamps[key]
|
|
2417
|
+
if key in self._cache_access_count:
|
|
2418
|
+
del self._cache_access_count[key]
|
|
2419
|
+
|
|
2420
|
+
logger.debug(f"Cleaned up {entries_to_remove} cache entries (size limit)")
|
|
2421
|
+
|
|
2422
|
+
# ==================== Streaming Support (Phase 7 - Tasks 1.15.11-1.15.12) ====================
|
|
2423
|
+
|
|
2424
|
+
async def execute_task_streaming(self, task: Dict[str, Any], context: Dict[str, Any]) -> AsyncIterator[Dict[str, Any]]:
|
|
2425
|
+
"""
|
|
2426
|
+
Execute a task with streaming results.
|
|
2427
|
+
|
|
2428
|
+
This method streams task execution events as they occur, including:
|
|
2429
|
+
- Status updates (started, thinking, acting, completed)
|
|
2430
|
+
- LLM tokens (for agents with LLM clients)
|
|
2431
|
+
- Tool calls and results (for agents with tools)
|
|
2432
|
+
- Final result
|
|
2433
|
+
|
|
2434
|
+
Args:
|
|
2435
|
+
task: Task specification
|
|
2436
|
+
context: Execution context
|
|
2437
|
+
|
|
2438
|
+
Yields:
|
|
2439
|
+
Dict[str, Any]: Event dictionaries with 'type' and event-specific data
|
|
2440
|
+
|
|
2441
|
+
Event types:
|
|
2442
|
+
- 'status': Status update (e.g., started, thinking, completed)
|
|
2443
|
+
- 'token': LLM token (for streaming text generation)
|
|
2444
|
+
- 'tool_call': Tool execution started
|
|
2445
|
+
- 'tool_result': Tool execution completed
|
|
2446
|
+
- 'result': Final task result
|
|
2447
|
+
- 'error': Error occurred
|
|
2448
|
+
|
|
2449
|
+
Example:
|
|
2450
|
+
```python
|
|
2451
|
+
async for event in agent.execute_task_streaming(task, context):
|
|
2452
|
+
if event['type'] == 'token':
|
|
2453
|
+
print(event['content'], end='', flush=True)
|
|
2454
|
+
elif event['type'] == 'tool_call':
|
|
2455
|
+
print(f"\\nCalling tool: {event['tool_name']}")
|
|
2456
|
+
elif event['type'] == 'tool_result':
|
|
2457
|
+
print(f"Tool result: {event['result']}")
|
|
2458
|
+
elif event['type'] == 'result':
|
|
2459
|
+
print(f"\\nFinal result: {event['output']}")
|
|
2460
|
+
```
|
|
2461
|
+
|
|
2462
|
+
Note:
|
|
2463
|
+
Subclasses should override this method to provide streaming support.
|
|
2464
|
+
Default implementation falls back to non-streaming execute_task.
|
|
2465
|
+
"""
|
|
2466
|
+
# Default implementation: execute task and yield result
|
|
2467
|
+
yield {"type": "status", "status": "started", "timestamp": datetime.utcnow().isoformat()}
|
|
2468
|
+
|
|
2469
|
+
try:
|
|
2470
|
+
result = await self.execute_task(task, context)
|
|
2471
|
+
yield {"type": "result", **result}
|
|
2472
|
+
except Exception as e:
|
|
2473
|
+
yield {
|
|
2474
|
+
"type": "error",
|
|
2475
|
+
"error": str(e),
|
|
2476
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
2477
|
+
}
|
|
2478
|
+
raise
|
|
2479
|
+
|
|
2480
|
+
async def process_message_streaming(self, message: str, sender_id: Optional[str] = None) -> AsyncIterator[str]:
|
|
2481
|
+
"""
|
|
2482
|
+
Process a message with streaming response.
|
|
2483
|
+
|
|
2484
|
+
This method streams the response text as it's generated, providing
|
|
2485
|
+
a better user experience for long responses.
|
|
2486
|
+
|
|
2487
|
+
Args:
|
|
2488
|
+
message: Message content
|
|
2489
|
+
sender_id: Optional sender identifier
|
|
2490
|
+
|
|
2491
|
+
Yields:
|
|
2492
|
+
str: Response text tokens/chunks
|
|
2493
|
+
|
|
2494
|
+
Example:
|
|
2495
|
+
```python
|
|
2496
|
+
async for token in agent.process_message_streaming("Hello!"):
|
|
2497
|
+
print(token, end='', flush=True)
|
|
2498
|
+
```
|
|
2499
|
+
|
|
2500
|
+
Note:
|
|
2501
|
+
Subclasses should override this method to provide streaming support.
|
|
2502
|
+
Default implementation falls back to non-streaming process_message.
|
|
2503
|
+
"""
|
|
2504
|
+
# Default implementation: process message and yield result
|
|
2505
|
+
try:
|
|
2506
|
+
result = await self.process_message(message, sender_id)
|
|
2507
|
+
response = result.get("response", "")
|
|
2508
|
+
yield response
|
|
2509
|
+
except Exception as e:
|
|
2510
|
+
logger.error(f"Streaming message processing failed: {e}")
|
|
2511
|
+
raise
|
|
2512
|
+
|
|
2513
|
+
# ==================== Agent Collaboration (Phase 7 - Tasks 1.15.15-1.15.22) ====================
|
|
2514
|
+
|
|
2515
|
+
async def delegate_task(
|
|
2516
|
+
self,
|
|
2517
|
+
task: Dict[str, Any],
|
|
2518
|
+
required_capabilities: Optional[List[str]] = None,
|
|
2519
|
+
target_agent_id: Optional[str] = None,
|
|
2520
|
+
) -> Dict[str, Any]:
|
|
2521
|
+
"""
|
|
2522
|
+
Delegate a task to another capable agent.
|
|
2523
|
+
|
|
2524
|
+
Args:
|
|
2525
|
+
task: Task specification to delegate
|
|
2526
|
+
required_capabilities: Required capabilities for the task
|
|
2527
|
+
target_agent_id: Specific agent to delegate to (if None, finds capable agent)
|
|
2528
|
+
|
|
2529
|
+
Returns:
|
|
2530
|
+
Task execution result from delegated agent
|
|
2531
|
+
|
|
2532
|
+
Raises:
|
|
2533
|
+
ValueError: If collaboration not enabled or no capable agent found
|
|
2534
|
+
|
|
2535
|
+
Example:
|
|
2536
|
+
```python
|
|
2537
|
+
# Delegate to specific agent
|
|
2538
|
+
result = await agent.delegate_task(
|
|
2539
|
+
task={"description": "Search for AI papers"},
|
|
2540
|
+
target_agent_id="search_agent"
|
|
2541
|
+
)
|
|
2542
|
+
|
|
2543
|
+
# Delegate to any capable agent
|
|
2544
|
+
result = await agent.delegate_task(
|
|
2545
|
+
task={"description": "Analyze data"},
|
|
2546
|
+
required_capabilities=["data_analysis", "statistics"]
|
|
2547
|
+
)
|
|
2548
|
+
```
|
|
2549
|
+
"""
|
|
2550
|
+
if not self._collaboration_enabled:
|
|
2551
|
+
raise ValueError("Agent collaboration is not enabled")
|
|
2552
|
+
|
|
2553
|
+
# Find target agent
|
|
2554
|
+
if target_agent_id:
|
|
2555
|
+
target_agent = self._agent_registry.get(target_agent_id)
|
|
2556
|
+
if not target_agent:
|
|
2557
|
+
raise ValueError(f"Agent {target_agent_id} not found in registry")
|
|
2558
|
+
elif required_capabilities:
|
|
2559
|
+
capable_agents = await self.find_capable_agents(required_capabilities)
|
|
2560
|
+
if not capable_agents:
|
|
2561
|
+
raise ValueError(f"No capable agents found for capabilities: {required_capabilities}")
|
|
2562
|
+
target_agent = capable_agents[0] # Use first capable agent
|
|
2563
|
+
else:
|
|
2564
|
+
raise ValueError("Either target_agent_id or required_capabilities must be provided")
|
|
2565
|
+
|
|
2566
|
+
logger.info(f"Agent {self.agent_id} delegating task to {target_agent.agent_id}")
|
|
2567
|
+
|
|
2568
|
+
# Delegate task
|
|
2569
|
+
try:
|
|
2570
|
+
result = await target_agent.execute_task(task, context={"delegated_by": self.agent_id})
|
|
2571
|
+
logger.info(f"Task delegation successful: {self.agent_id} -> {target_agent.agent_id}")
|
|
2572
|
+
return result
|
|
2573
|
+
except Exception as e:
|
|
2574
|
+
logger.error(f"Task delegation failed: {e}")
|
|
2575
|
+
raise
|
|
2576
|
+
|
|
2577
|
+
async def find_capable_agents(self, required_capabilities: List[str]) -> List[Any]:
|
|
2578
|
+
"""
|
|
2579
|
+
Find agents with required capabilities.
|
|
2580
|
+
|
|
2581
|
+
Args:
|
|
2582
|
+
required_capabilities: List of required capability names
|
|
2583
|
+
|
|
2584
|
+
Returns:
|
|
2585
|
+
List of agents that have all required capabilities
|
|
2586
|
+
|
|
2587
|
+
Example:
|
|
2588
|
+
```python
|
|
2589
|
+
agents = await agent.find_capable_agents(["search", "summarize"])
|
|
2590
|
+
for capable_agent in agents:
|
|
2591
|
+
print(f"Found: {capable_agent.name}")
|
|
2592
|
+
```
|
|
2593
|
+
"""
|
|
2594
|
+
if not self._collaboration_enabled:
|
|
2595
|
+
return []
|
|
2596
|
+
|
|
2597
|
+
capable_agents = []
|
|
2598
|
+
for agent_id, agent in self._agent_registry.items():
|
|
2599
|
+
# Skip self
|
|
2600
|
+
if agent_id == self.agent_id:
|
|
2601
|
+
continue
|
|
2602
|
+
|
|
2603
|
+
# Check if agent has all required capabilities
|
|
2604
|
+
agent_capabilities = getattr(agent, "capabilities", [])
|
|
2605
|
+
if all(cap in agent_capabilities for cap in required_capabilities):
|
|
2606
|
+
capable_agents.append(agent)
|
|
2607
|
+
|
|
2608
|
+
logger.debug(f"Found {len(capable_agents)} capable agents for {required_capabilities}")
|
|
2609
|
+
return capable_agents
|
|
2610
|
+
|
|
2611
|
+
async def request_peer_review(
|
|
2612
|
+
self,
|
|
2613
|
+
task: Dict[str, Any],
|
|
2614
|
+
result: Dict[str, Any],
|
|
2615
|
+
reviewer_id: Optional[str] = None,
|
|
2616
|
+
) -> Dict[str, Any]:
|
|
2617
|
+
"""
|
|
2618
|
+
Request peer review of a task result.
|
|
2619
|
+
|
|
2620
|
+
Args:
|
|
2621
|
+
task: Original task specification
|
|
2622
|
+
result: Task execution result to review
|
|
2623
|
+
reviewer_id: Specific reviewer agent ID (if None, selects automatically)
|
|
2624
|
+
|
|
2625
|
+
Returns:
|
|
2626
|
+
Review result with 'approved' (bool), 'feedback' (str), 'reviewer_id' (str)
|
|
2627
|
+
|
|
2628
|
+
Example:
|
|
2629
|
+
```python
|
|
2630
|
+
result = await agent.execute_task(task, context)
|
|
2631
|
+
review = await agent.request_peer_review(task, result)
|
|
2632
|
+
if review['approved']:
|
|
2633
|
+
print(f"Approved: {review['feedback']}")
|
|
2634
|
+
else:
|
|
2635
|
+
print(f"Needs revision: {review['feedback']}")
|
|
2636
|
+
```
|
|
2637
|
+
"""
|
|
2638
|
+
if not self._collaboration_enabled:
|
|
2639
|
+
raise ValueError("Agent collaboration is not enabled")
|
|
2640
|
+
|
|
2641
|
+
# Find reviewer
|
|
2642
|
+
if reviewer_id:
|
|
2643
|
+
reviewer = self._agent_registry.get(reviewer_id)
|
|
2644
|
+
if not reviewer:
|
|
2645
|
+
raise ValueError(f"Reviewer {reviewer_id} not found in registry")
|
|
2646
|
+
else:
|
|
2647
|
+
# Select first available agent (excluding self)
|
|
2648
|
+
available_reviewers = [agent for agent_id, agent in self._agent_registry.items() if agent_id != self.agent_id]
|
|
2649
|
+
if not available_reviewers:
|
|
2650
|
+
raise ValueError("No reviewers available")
|
|
2651
|
+
reviewer = available_reviewers[0]
|
|
2652
|
+
|
|
2653
|
+
logger.info(f"Agent {self.agent_id} requesting review from {reviewer.agent_id}")
|
|
2654
|
+
|
|
2655
|
+
# Request review
|
|
2656
|
+
try:
|
|
2657
|
+
if hasattr(reviewer, "review_result"):
|
|
2658
|
+
review = await reviewer.review_result(task, result)
|
|
2659
|
+
else:
|
|
2660
|
+
# Fallback: use execute_task with review prompt
|
|
2661
|
+
task_desc = task.get("description", "")
|
|
2662
|
+
task_result = result.get("output", "")
|
|
2663
|
+
review_task = {
|
|
2664
|
+
"description": (f"Review this task result:\nTask: {task_desc}\nResult: {task_result}"),
|
|
2665
|
+
"task_id": f"review_{task.get('task_id', 'unknown')}",
|
|
2666
|
+
}
|
|
2667
|
+
review_result = await reviewer.execute_task(review_task, context={})
|
|
2668
|
+
review = {
|
|
2669
|
+
"approved": True, # Assume approved if no explicit review method
|
|
2670
|
+
"feedback": review_result.get("output", ""),
|
|
2671
|
+
"reviewer_id": reviewer.agent_id,
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
logger.info(f"Review received from {reviewer.agent_id}")
|
|
2675
|
+
return review
|
|
2676
|
+
except Exception as e:
|
|
2677
|
+
logger.error(f"Peer review failed: {e}")
|
|
2678
|
+
raise
|
|
2679
|
+
|
|
2680
|
+
async def collaborate_on_task(
|
|
2681
|
+
self,
|
|
2682
|
+
task: Dict[str, Any],
|
|
2683
|
+
collaborator_ids: List[str],
|
|
2684
|
+
strategy: str = "parallel",
|
|
2685
|
+
) -> Dict[str, Any]:
|
|
2686
|
+
"""
|
|
2687
|
+
Collaborate with other agents on a task.
|
|
2688
|
+
|
|
2689
|
+
Args:
|
|
2690
|
+
task: Task specification
|
|
2691
|
+
collaborator_ids: List of agent IDs to collaborate with
|
|
2692
|
+
strategy: Collaboration strategy - 'parallel', 'sequential', or 'consensus'
|
|
2693
|
+
|
|
2694
|
+
Returns:
|
|
2695
|
+
Aggregated result based on strategy
|
|
2696
|
+
|
|
2697
|
+
Strategies:
|
|
2698
|
+
- parallel: All agents work simultaneously, results aggregated
|
|
2699
|
+
- sequential: Agents work in order, each building on previous results
|
|
2700
|
+
- consensus: All agents work independently, best result selected by voting
|
|
2701
|
+
|
|
2702
|
+
Example:
|
|
2703
|
+
```python
|
|
2704
|
+
# Parallel collaboration
|
|
2705
|
+
result = await agent.collaborate_on_task(
|
|
2706
|
+
task={"description": "Analyze market trends"},
|
|
2707
|
+
collaborator_ids=["analyst1", "analyst2", "analyst3"],
|
|
2708
|
+
strategy="parallel"
|
|
2709
|
+
)
|
|
2710
|
+
|
|
2711
|
+
# Sequential collaboration (pipeline)
|
|
2712
|
+
result = await agent.collaborate_on_task(
|
|
2713
|
+
task={"description": "Research and summarize"},
|
|
2714
|
+
collaborator_ids=["researcher", "summarizer"],
|
|
2715
|
+
strategy="sequential"
|
|
2716
|
+
)
|
|
2717
|
+
|
|
2718
|
+
# Consensus collaboration
|
|
2719
|
+
result = await agent.collaborate_on_task(
|
|
2720
|
+
task={"description": "Make recommendation"},
|
|
2721
|
+
collaborator_ids=["expert1", "expert2", "expert3"],
|
|
2722
|
+
strategy="consensus"
|
|
2723
|
+
)
|
|
2724
|
+
```
|
|
2725
|
+
"""
|
|
2726
|
+
if not self._collaboration_enabled:
|
|
2727
|
+
raise ValueError("Agent collaboration is not enabled")
|
|
2728
|
+
|
|
2729
|
+
# Get collaborator agents
|
|
2730
|
+
collaborators = []
|
|
2731
|
+
for agent_id in collaborator_ids:
|
|
2732
|
+
agent = self._agent_registry.get(agent_id)
|
|
2733
|
+
if not agent:
|
|
2734
|
+
logger.warning(f"Collaborator {agent_id} not found, skipping")
|
|
2735
|
+
continue
|
|
2736
|
+
collaborators.append(agent)
|
|
2737
|
+
|
|
2738
|
+
if not collaborators:
|
|
2739
|
+
raise ValueError("No valid collaborators found")
|
|
2740
|
+
|
|
2741
|
+
logger.info(f"Agent {self.agent_id} collaborating with {len(collaborators)} agents " f"using {strategy} strategy")
|
|
2742
|
+
|
|
2743
|
+
# Execute based on strategy
|
|
2744
|
+
if strategy == "parallel":
|
|
2745
|
+
return await self._collaborate_parallel(task, collaborators)
|
|
2746
|
+
elif strategy == "sequential":
|
|
2747
|
+
return await self._collaborate_sequential(task, collaborators)
|
|
2748
|
+
elif strategy == "consensus":
|
|
2749
|
+
return await self._collaborate_consensus(task, collaborators)
|
|
2750
|
+
else:
|
|
2751
|
+
raise ValueError(f"Unknown collaboration strategy: {strategy}")
|
|
2752
|
+
|
|
2753
|
+
async def _collaborate_parallel(self, task: Dict[str, Any], collaborators: List[Any]) -> Dict[str, Any]:
|
|
2754
|
+
"""
|
|
2755
|
+
Parallel collaboration: all agents work simultaneously.
|
|
2756
|
+
|
|
2757
|
+
Args:
|
|
2758
|
+
task: Task specification
|
|
2759
|
+
collaborators: List of collaborator agents
|
|
2760
|
+
|
|
2761
|
+
Returns:
|
|
2762
|
+
Aggregated result
|
|
2763
|
+
"""
|
|
2764
|
+
# Execute task on all agents in parallel
|
|
2765
|
+
tasks = [agent.execute_task(task, context={"collaboration": "parallel"}) for agent in collaborators]
|
|
2766
|
+
|
|
2767
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
2768
|
+
|
|
2769
|
+
# Aggregate results
|
|
2770
|
+
return await self._aggregate_results(task, results, collaborators)
|
|
2771
|
+
|
|
2772
|
+
async def _collaborate_sequential(self, task: Dict[str, Any], collaborators: List[Any]) -> Dict[str, Any]:
|
|
2773
|
+
"""
|
|
2774
|
+
Sequential collaboration: agents work in order, building on previous results.
|
|
2775
|
+
|
|
2776
|
+
Args:
|
|
2777
|
+
task: Task specification
|
|
2778
|
+
collaborators: List of collaborator agents (in execution order)
|
|
2779
|
+
|
|
2780
|
+
Returns:
|
|
2781
|
+
Final result from last agent
|
|
2782
|
+
"""
|
|
2783
|
+
current_task = task.copy()
|
|
2784
|
+
results = []
|
|
2785
|
+
|
|
2786
|
+
for i, agent in enumerate(collaborators):
|
|
2787
|
+
logger.debug(f"Sequential step {i + 1}/{len(collaborators)}: {agent.agent_id}")
|
|
2788
|
+
|
|
2789
|
+
# Execute task
|
|
2790
|
+
result = await agent.execute_task(current_task, context={"collaboration": "sequential", "step": i + 1})
|
|
2791
|
+
results.append(result)
|
|
2792
|
+
|
|
2793
|
+
# Update task for next agent with previous result
|
|
2794
|
+
if i < len(collaborators) - 1:
|
|
2795
|
+
current_task = {
|
|
2796
|
+
"description": f"{task.get('description')}\n\nPrevious result: {result.get('output')}",
|
|
2797
|
+
"task_id": f"{task.get('task_id', 'unknown')}_step_{i + 2}",
|
|
2798
|
+
}
|
|
2799
|
+
|
|
2800
|
+
# Return final result
|
|
2801
|
+
return {
|
|
2802
|
+
"success": True,
|
|
2803
|
+
"output": results[-1].get("output") if results else "",
|
|
2804
|
+
"collaboration_strategy": "sequential",
|
|
2805
|
+
"steps": len(results),
|
|
2806
|
+
"all_results": results,
|
|
2807
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
async def _collaborate_consensus(self, task: Dict[str, Any], collaborators: List[Any]) -> Dict[str, Any]:
|
|
2811
|
+
"""
|
|
2812
|
+
Consensus collaboration: all agents work independently, best result selected.
|
|
2813
|
+
|
|
2814
|
+
Args:
|
|
2815
|
+
task: Task specification
|
|
2816
|
+
collaborators: List of collaborator agents
|
|
2817
|
+
|
|
2818
|
+
Returns:
|
|
2819
|
+
Best result selected by consensus
|
|
2820
|
+
"""
|
|
2821
|
+
# Execute task on all agents in parallel
|
|
2822
|
+
tasks = [agent.execute_task(task, context={"collaboration": "consensus"}) for agent in collaborators]
|
|
2823
|
+
|
|
2824
|
+
results = await asyncio.gather(*tasks, return_exceptions=True)
|
|
2825
|
+
|
|
2826
|
+
# Select best result by consensus
|
|
2827
|
+
return await self._select_consensus_result(task, results, collaborators)
|
|
2828
|
+
|
|
2829
|
+
async def _aggregate_results(self, task: Dict[str, Any], results: List[Any], collaborators: List[Any]) -> Dict[str, Any]:
|
|
2830
|
+
"""
|
|
2831
|
+
Aggregate results from parallel collaboration.
|
|
2832
|
+
|
|
2833
|
+
Args:
|
|
2834
|
+
task: Original task
|
|
2835
|
+
results: List of results from collaborators
|
|
2836
|
+
collaborators: List of collaborator agents
|
|
2837
|
+
|
|
2838
|
+
Returns:
|
|
2839
|
+
Aggregated result
|
|
2840
|
+
"""
|
|
2841
|
+
successful_results = []
|
|
2842
|
+
errors = []
|
|
2843
|
+
|
|
2844
|
+
for i, result in enumerate(results):
|
|
2845
|
+
if isinstance(result, Exception):
|
|
2846
|
+
errors.append({"agent": collaborators[i].agent_id, "error": str(result)})
|
|
2847
|
+
else:
|
|
2848
|
+
successful_results.append({"agent": collaborators[i].agent_id, "result": result})
|
|
2849
|
+
|
|
2850
|
+
# Combine outputs
|
|
2851
|
+
combined_output = "\n\n".join([f"[{r['agent']}]: {r['result'].get('output', '')}" for r in successful_results])
|
|
2852
|
+
|
|
2853
|
+
return {
|
|
2854
|
+
"success": len(successful_results) > 0,
|
|
2855
|
+
"output": combined_output,
|
|
2856
|
+
"collaboration_strategy": "parallel",
|
|
2857
|
+
"successful_agents": len(successful_results),
|
|
2858
|
+
"failed_agents": len(errors),
|
|
2859
|
+
"results": successful_results,
|
|
2860
|
+
"errors": errors if errors else None,
|
|
2861
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
2862
|
+
}
|
|
2863
|
+
|
|
2864
|
+
async def _select_consensus_result(self, task: Dict[str, Any], results: List[Any], collaborators: List[Any]) -> Dict[str, Any]:
|
|
2865
|
+
"""
|
|
2866
|
+
Select best result by consensus voting.
|
|
2867
|
+
|
|
2868
|
+
Args:
|
|
2869
|
+
task: Original task
|
|
2870
|
+
results: List of results from collaborators
|
|
2871
|
+
collaborators: List of collaborator agents
|
|
2872
|
+
|
|
2873
|
+
Returns:
|
|
2874
|
+
Best result selected by consensus
|
|
2875
|
+
"""
|
|
2876
|
+
successful_results = []
|
|
2877
|
+
|
|
2878
|
+
for i, result in enumerate(results):
|
|
2879
|
+
if not isinstance(result, Exception):
|
|
2880
|
+
successful_results.append({"agent": collaborators[i].agent_id, "result": result, "votes": 0})
|
|
2881
|
+
|
|
2882
|
+
if not successful_results:
|
|
2883
|
+
return {
|
|
2884
|
+
"success": False,
|
|
2885
|
+
"output": "All collaborators failed",
|
|
2886
|
+
"collaboration_strategy": "consensus",
|
|
2887
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
2888
|
+
}
|
|
2889
|
+
|
|
2890
|
+
# Simple voting: each agent votes for best result (excluding their own)
|
|
2891
|
+
# In a real implementation, this could use LLM to evaluate quality
|
|
2892
|
+
for voter_idx, voter_result in enumerate(successful_results):
|
|
2893
|
+
# For now, use simple heuristic: longest output is "best"
|
|
2894
|
+
# In production, use LLM-based evaluation
|
|
2895
|
+
best_idx = max(
|
|
2896
|
+
range(len(successful_results)),
|
|
2897
|
+
key=lambda i: (len(successful_results[i]["result"].get("output", "")) if i != voter_idx else 0),
|
|
2898
|
+
)
|
|
2899
|
+
successful_results[best_idx]["votes"] += 1
|
|
2900
|
+
|
|
2901
|
+
# Select result with most votes
|
|
2902
|
+
best_result = max(successful_results, key=lambda r: r["votes"])
|
|
2903
|
+
|
|
2904
|
+
return {
|
|
2905
|
+
"success": True,
|
|
2906
|
+
"output": best_result["result"].get("output", ""),
|
|
2907
|
+
"collaboration_strategy": "consensus",
|
|
2908
|
+
"selected_agent": best_result["agent"],
|
|
2909
|
+
"votes": best_result["votes"],
|
|
2910
|
+
"total_agents": len(successful_results),
|
|
2911
|
+
"all_results": successful_results,
|
|
2912
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
# ==================== Smart Context Management (Phase 8 - Tasks 1.16.1-1.16.3) ====================
|
|
2916
|
+
|
|
2917
|
+
async def get_relevant_context(
|
|
2918
|
+
self,
|
|
2919
|
+
query: str,
|
|
2920
|
+
context_items: List[Dict[str, Any]],
|
|
2921
|
+
max_items: Optional[int] = None,
|
|
2922
|
+
min_relevance_score: float = 0.5,
|
|
2923
|
+
) -> List[Dict[str, Any]]:
|
|
2924
|
+
"""
|
|
2925
|
+
Get relevant context items using semantic search and relevance scoring.
|
|
2926
|
+
|
|
2927
|
+
This method filters and ranks context items based on their relevance to
|
|
2928
|
+
the query, helping agents stay within token limits while maintaining
|
|
2929
|
+
the most important context.
|
|
2930
|
+
|
|
2931
|
+
Args:
|
|
2932
|
+
query: Query or task description to match against
|
|
2933
|
+
context_items: List of context items (dicts with 'content' field)
|
|
2934
|
+
max_items: Maximum number of items to return (None = no limit)
|
|
2935
|
+
min_relevance_score: Minimum relevance score (0.0-1.0)
|
|
2936
|
+
|
|
2937
|
+
Returns:
|
|
2938
|
+
List of relevant context items, sorted by relevance (highest first)
|
|
2939
|
+
|
|
2940
|
+
Example:
|
|
2941
|
+
```python
|
|
2942
|
+
context_items = [
|
|
2943
|
+
{"content": "User prefers concise answers", "type": "preference"},
|
|
2944
|
+
{"content": "Previous task: data analysis", "type": "history"},
|
|
2945
|
+
{"content": "System configuration: prod", "type": "config"},
|
|
2946
|
+
]
|
|
2947
|
+
|
|
2948
|
+
relevant = await agent.get_relevant_context(
|
|
2949
|
+
query="Analyze sales data",
|
|
2950
|
+
context_items=context_items,
|
|
2951
|
+
max_items=2,
|
|
2952
|
+
min_relevance_score=0.6
|
|
2953
|
+
)
|
|
2954
|
+
# Returns top 2 most relevant items with score >= 0.6
|
|
2955
|
+
```
|
|
2956
|
+
"""
|
|
2957
|
+
if not context_items:
|
|
2958
|
+
return []
|
|
2959
|
+
|
|
2960
|
+
# Score all items
|
|
2961
|
+
scored_items = []
|
|
2962
|
+
for item in context_items:
|
|
2963
|
+
score = await self.score_context_relevance(query, item)
|
|
2964
|
+
if score >= min_relevance_score:
|
|
2965
|
+
scored_items.append({**item, "_relevance_score": score})
|
|
2966
|
+
|
|
2967
|
+
# Sort by relevance (highest first)
|
|
2968
|
+
scored_items.sort(key=lambda x: x["_relevance_score"], reverse=True)
|
|
2969
|
+
|
|
2970
|
+
# Limit number of items
|
|
2971
|
+
if max_items is not None:
|
|
2972
|
+
scored_items = scored_items[:max_items]
|
|
2973
|
+
|
|
2974
|
+
logger.debug(f"Selected {len(scored_items)}/{len(context_items)} relevant context items " f"(min_score={min_relevance_score})")
|
|
2975
|
+
|
|
2976
|
+
return scored_items
|
|
2977
|
+
|
|
2978
|
+
async def score_context_relevance(self, query: str, context_item: Dict[str, Any]) -> float:
|
|
2979
|
+
"""
|
|
2980
|
+
Score the relevance of a context item to a query.
|
|
2981
|
+
|
|
2982
|
+
Uses multiple signals to determine relevance:
|
|
2983
|
+
- Keyword overlap (basic)
|
|
2984
|
+
- Semantic similarity (if LLM client with embeddings available)
|
|
2985
|
+
- Recency (if timestamp available)
|
|
2986
|
+
- Type priority (if type specified)
|
|
2987
|
+
|
|
2988
|
+
Args:
|
|
2989
|
+
query: Query or task description
|
|
2990
|
+
context_item: Context item to score (dict with 'content' field)
|
|
2991
|
+
|
|
2992
|
+
Returns:
|
|
2993
|
+
Relevance score between 0.0 (not relevant) and 1.0 (highly relevant)
|
|
2994
|
+
|
|
2995
|
+
Example:
|
|
2996
|
+
```python
|
|
2997
|
+
score = await agent.score_context_relevance(
|
|
2998
|
+
query="Analyze sales data",
|
|
2999
|
+
context_item={"content": "Previous analysis results", "type": "history"}
|
|
3000
|
+
)
|
|
3001
|
+
print(f"Relevance: {score:.2f}")
|
|
3002
|
+
```
|
|
3003
|
+
"""
|
|
3004
|
+
content = context_item.get("content", "")
|
|
3005
|
+
if not content:
|
|
3006
|
+
return 0.0
|
|
3007
|
+
|
|
3008
|
+
# Convert to lowercase for comparison
|
|
3009
|
+
query_lower = query.lower()
|
|
3010
|
+
content_lower = content.lower()
|
|
3011
|
+
|
|
3012
|
+
# 1. Keyword overlap score (0.0-0.5)
|
|
3013
|
+
query_words = set(query_lower.split())
|
|
3014
|
+
content_words = set(content_lower.split())
|
|
3015
|
+
if not query_words:
|
|
3016
|
+
keyword_score = 0.0
|
|
3017
|
+
else:
|
|
3018
|
+
overlap = len(query_words & content_words)
|
|
3019
|
+
keyword_score = min(0.5, (overlap / len(query_words)) * 0.5)
|
|
3020
|
+
|
|
3021
|
+
# 2. Semantic similarity score (0.0-0.3)
|
|
3022
|
+
# If LLM client with embeddings is available, use it
|
|
3023
|
+
semantic_score = 0.0
|
|
3024
|
+
if self._llm_client and hasattr(self._llm_client, "get_embeddings"):
|
|
3025
|
+
try:
|
|
3026
|
+
embeddings = await self._llm_client.get_embeddings([query, content])
|
|
3027
|
+
if len(embeddings) == 2:
|
|
3028
|
+
# Calculate cosine similarity
|
|
3029
|
+
import math
|
|
3030
|
+
|
|
3031
|
+
vec1, vec2 = embeddings[0], embeddings[1]
|
|
3032
|
+
dot_product = sum(a * b for a, b in zip(vec1, vec2))
|
|
3033
|
+
mag1 = math.sqrt(sum(a * a for a in vec1))
|
|
3034
|
+
mag2 = math.sqrt(sum(b * b for b in vec2))
|
|
3035
|
+
if mag1 > 0 and mag2 > 0:
|
|
3036
|
+
similarity = dot_product / (mag1 * mag2)
|
|
3037
|
+
semantic_score = max(0.0, similarity) * 0.3
|
|
3038
|
+
except Exception as e:
|
|
3039
|
+
logger.debug(f"Semantic similarity calculation failed: {e}")
|
|
3040
|
+
|
|
3041
|
+
# 3. Recency score (0.0-0.1)
|
|
3042
|
+
recency_score = 0.0
|
|
3043
|
+
if "timestamp" in context_item:
|
|
3044
|
+
try:
|
|
3045
|
+
from datetime import datetime
|
|
3046
|
+
|
|
3047
|
+
timestamp = context_item["timestamp"]
|
|
3048
|
+
if isinstance(timestamp, str):
|
|
3049
|
+
timestamp = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
|
|
3050
|
+
age_seconds = (datetime.utcnow() - timestamp).total_seconds()
|
|
3051
|
+
# Decay over 24 hours
|
|
3052
|
+
recency_score = max(0.0, 0.1 * (1.0 - min(1.0, age_seconds / 86400)))
|
|
3053
|
+
except Exception as e:
|
|
3054
|
+
logger.debug(f"Recency calculation failed: {e}")
|
|
3055
|
+
|
|
3056
|
+
# 4. Type priority score (0.0-0.1)
|
|
3057
|
+
type_score = 0.0
|
|
3058
|
+
item_type = context_item.get("type", "")
|
|
3059
|
+
priority_types = {"preference": 0.1, "constraint": 0.1, "requirement": 0.09}
|
|
3060
|
+
type_score = priority_types.get(item_type, 0.05)
|
|
3061
|
+
|
|
3062
|
+
# Combine scores
|
|
3063
|
+
total_score = keyword_score + semantic_score + recency_score + type_score
|
|
3064
|
+
|
|
3065
|
+
return min(1.0, total_score)
|
|
3066
|
+
|
|
3067
|
+
async def prune_context(
|
|
3068
|
+
self,
|
|
3069
|
+
context_items: List[Dict[str, Any]],
|
|
3070
|
+
max_tokens: int,
|
|
3071
|
+
query: Optional[str] = None,
|
|
3072
|
+
preserve_types: Optional[List[str]] = None,
|
|
3073
|
+
) -> List[Dict[str, Any]]:
|
|
3074
|
+
"""
|
|
3075
|
+
Prune context items to fit within token limit.
|
|
3076
|
+
|
|
3077
|
+
Uses relevance scoring to keep the most important context while
|
|
3078
|
+
staying within token limits. Optionally preserves certain types
|
|
3079
|
+
of context regardless of relevance.
|
|
3080
|
+
|
|
3081
|
+
Args:
|
|
3082
|
+
context_items: List of context items to prune
|
|
3083
|
+
max_tokens: Maximum total tokens allowed
|
|
3084
|
+
query: Optional query for relevance scoring
|
|
3085
|
+
preserve_types: Optional list of types to always preserve
|
|
3086
|
+
|
|
3087
|
+
Returns:
|
|
3088
|
+
Pruned list of context items that fit within token limit
|
|
3089
|
+
|
|
3090
|
+
Example:
|
|
3091
|
+
```python
|
|
3092
|
+
pruned = await agent.prune_context(
|
|
3093
|
+
context_items=all_context,
|
|
3094
|
+
max_tokens=2000,
|
|
3095
|
+
query="Analyze data",
|
|
3096
|
+
preserve_types=["constraint", "requirement"]
|
|
3097
|
+
)
|
|
3098
|
+
print(f"Pruned from {len(all_context)} to {len(pruned)} items")
|
|
3099
|
+
```
|
|
3100
|
+
"""
|
|
3101
|
+
if not context_items:
|
|
3102
|
+
return []
|
|
3103
|
+
|
|
3104
|
+
preserve_types = preserve_types or []
|
|
3105
|
+
|
|
3106
|
+
# Separate preserved and regular items
|
|
3107
|
+
preserved_items = []
|
|
3108
|
+
regular_items = []
|
|
3109
|
+
|
|
3110
|
+
for item in context_items:
|
|
3111
|
+
if item.get("type") in preserve_types:
|
|
3112
|
+
preserved_items.append(item)
|
|
3113
|
+
else:
|
|
3114
|
+
regular_items.append(item)
|
|
3115
|
+
|
|
3116
|
+
# Score regular items if query provided
|
|
3117
|
+
if query and regular_items:
|
|
3118
|
+
scored_items = []
|
|
3119
|
+
for item in regular_items:
|
|
3120
|
+
score = await self.score_context_relevance(query, item)
|
|
3121
|
+
scored_items.append({**item, "_relevance_score": score})
|
|
3122
|
+
# Sort by relevance
|
|
3123
|
+
scored_items.sort(key=lambda x: x["_relevance_score"], reverse=True)
|
|
3124
|
+
regular_items = scored_items
|
|
3125
|
+
|
|
3126
|
+
# Estimate tokens (rough approximation: 1 token ≈ 4 characters)
|
|
3127
|
+
def estimate_tokens(item: Dict[str, Any]) -> int:
|
|
3128
|
+
content = str(item.get("content", ""))
|
|
3129
|
+
return len(content) // 4
|
|
3130
|
+
|
|
3131
|
+
# Add preserved items first
|
|
3132
|
+
result = []
|
|
3133
|
+
current_tokens = 0
|
|
3134
|
+
|
|
3135
|
+
for item in preserved_items:
|
|
3136
|
+
item_tokens = estimate_tokens(item)
|
|
3137
|
+
if current_tokens + item_tokens <= max_tokens:
|
|
3138
|
+
result.append(item)
|
|
3139
|
+
current_tokens += item_tokens
|
|
3140
|
+
else:
|
|
3141
|
+
logger.warning(f"Preserved item exceeds token limit, skipping: {item.get('type')}")
|
|
3142
|
+
|
|
3143
|
+
# Add regular items until token limit
|
|
3144
|
+
for item in regular_items:
|
|
3145
|
+
item_tokens = estimate_tokens(item)
|
|
3146
|
+
if current_tokens + item_tokens <= max_tokens:
|
|
3147
|
+
result.append(item)
|
|
3148
|
+
current_tokens += item_tokens
|
|
3149
|
+
else:
|
|
3150
|
+
break
|
|
3151
|
+
|
|
3152
|
+
logger.info(f"Pruned context from {len(context_items)} to {len(result)} items " f"({current_tokens}/{max_tokens} tokens)")
|
|
3153
|
+
|
|
3154
|
+
return result
|
|
3155
|
+
|
|
3156
|
+
# ==================== Agent Learning (Phase 8 - Tasks 1.16.4-1.16.10) ====================
|
|
3157
|
+
|
|
3158
|
+
async def record_experience(
|
|
3159
|
+
self,
|
|
3160
|
+
task: Dict[str, Any],
|
|
3161
|
+
result: Dict[str, Any],
|
|
3162
|
+
approach: str,
|
|
3163
|
+
tools_used: Optional[List[str]] = None,
|
|
3164
|
+
) -> None:
|
|
3165
|
+
"""
|
|
3166
|
+
Record an experience for learning and adaptation.
|
|
3167
|
+
|
|
3168
|
+
Args:
|
|
3169
|
+
task: Task specification
|
|
3170
|
+
result: Task execution result
|
|
3171
|
+
approach: Approach/strategy used
|
|
3172
|
+
tools_used: List of tools used (if any)
|
|
3173
|
+
|
|
3174
|
+
Example:
|
|
3175
|
+
```python
|
|
3176
|
+
await agent.record_experience(
|
|
3177
|
+
task={"description": "Analyze data", "type": "analysis"},
|
|
3178
|
+
result={"success": True, "execution_time": 5.2},
|
|
3179
|
+
approach="statistical_analysis",
|
|
3180
|
+
tools_used=["pandas", "numpy"]
|
|
3181
|
+
)
|
|
3182
|
+
```
|
|
3183
|
+
"""
|
|
3184
|
+
if not self._learning_enabled:
|
|
3185
|
+
return
|
|
3186
|
+
|
|
3187
|
+
from .models import Experience
|
|
3188
|
+
|
|
3189
|
+
# Classify task
|
|
3190
|
+
task_type = await self._classify_task(task)
|
|
3191
|
+
|
|
3192
|
+
# Create experience record
|
|
3193
|
+
experience = Experience( # type: ignore[call-arg]
|
|
3194
|
+
agent_id=self.agent_id,
|
|
3195
|
+
task_type=task_type,
|
|
3196
|
+
task_description=task.get("description", ""),
|
|
3197
|
+
task_complexity=task.get("complexity"),
|
|
3198
|
+
approach=approach,
|
|
3199
|
+
tools_used=tools_used or [],
|
|
3200
|
+
execution_time=result.get("execution_time", 0.0),
|
|
3201
|
+
success=result.get("success", False),
|
|
3202
|
+
quality_score=result.get("quality_score"),
|
|
3203
|
+
error_type=result.get("error_type"),
|
|
3204
|
+
error_message=result.get("error"),
|
|
3205
|
+
context_size=result.get("context_size"),
|
|
3206
|
+
iterations=result.get("iterations"),
|
|
3207
|
+
metadata={"task_id": task.get("task_id")},
|
|
3208
|
+
)
|
|
3209
|
+
|
|
3210
|
+
# Add to experiences
|
|
3211
|
+
self._experiences.append(experience)
|
|
3212
|
+
|
|
3213
|
+
# Limit stored experiences
|
|
3214
|
+
if len(self._experiences) > self._max_experiences:
|
|
3215
|
+
self._experiences = self._experiences[-self._max_experiences :]
|
|
3216
|
+
|
|
3217
|
+
logger.debug(f"Recorded experience: {task_type} - " f"{'success' if experience.success else 'failure'} " f"({experience.execution_time:.2f}s)")
|
|
3218
|
+
|
|
3219
|
+
async def get_recommended_approach(self, task: Dict[str, Any]) -> Optional[Dict[str, Any]]:
|
|
3220
|
+
"""
|
|
3221
|
+
Get recommended approach based on past experiences.
|
|
3222
|
+
|
|
3223
|
+
Analyzes similar past experiences to recommend the best approach
|
|
3224
|
+
for the current task.
|
|
3225
|
+
|
|
3226
|
+
Args:
|
|
3227
|
+
task: Task specification
|
|
3228
|
+
|
|
3229
|
+
Returns:
|
|
3230
|
+
Recommended approach dict with 'approach', 'confidence', 'reasoning'
|
|
3231
|
+
or None if no relevant experiences
|
|
3232
|
+
|
|
3233
|
+
Example:
|
|
3234
|
+
```python
|
|
3235
|
+
recommendation = await agent.get_recommended_approach(
|
|
3236
|
+
task={"description": "Analyze sales data", "type": "analysis"}
|
|
3237
|
+
)
|
|
3238
|
+
if recommendation:
|
|
3239
|
+
print(f"Recommended: {recommendation['approach']}")
|
|
3240
|
+
print(f"Confidence: {recommendation['confidence']:.2f}")
|
|
3241
|
+
print(f"Reasoning: {recommendation['reasoning']}")
|
|
3242
|
+
```
|
|
3243
|
+
"""
|
|
3244
|
+
if not self._learning_enabled or not self._experiences:
|
|
3245
|
+
return None
|
|
3246
|
+
|
|
3247
|
+
# Classify current task
|
|
3248
|
+
task_type = await self._classify_task(task)
|
|
3249
|
+
|
|
3250
|
+
# Find similar experiences
|
|
3251
|
+
similar_experiences = [exp for exp in self._experiences if exp.task_type == task_type]
|
|
3252
|
+
|
|
3253
|
+
if not similar_experiences:
|
|
3254
|
+
return None
|
|
3255
|
+
|
|
3256
|
+
# Analyze successful experiences
|
|
3257
|
+
successful = [exp for exp in similar_experiences if exp.success]
|
|
3258
|
+
if not successful:
|
|
3259
|
+
return None
|
|
3260
|
+
|
|
3261
|
+
# Count approaches
|
|
3262
|
+
approach_stats: Dict[str, Dict[str, Any]] = {}
|
|
3263
|
+
for exp in successful:
|
|
3264
|
+
if exp.approach not in approach_stats:
|
|
3265
|
+
approach_stats[exp.approach] = {
|
|
3266
|
+
"count": 0,
|
|
3267
|
+
"total_time": 0.0,
|
|
3268
|
+
"avg_quality": 0.0,
|
|
3269
|
+
"quality_count": 0,
|
|
3270
|
+
}
|
|
3271
|
+
stats = approach_stats[exp.approach]
|
|
3272
|
+
stats["count"] += 1
|
|
3273
|
+
stats["total_time"] += exp.execution_time
|
|
3274
|
+
if exp.quality_score is not None:
|
|
3275
|
+
stats["avg_quality"] += exp.quality_score
|
|
3276
|
+
stats["quality_count"] += 1
|
|
3277
|
+
|
|
3278
|
+
# Calculate averages and scores
|
|
3279
|
+
for approach, stats in approach_stats.items():
|
|
3280
|
+
stats["avg_time"] = stats["total_time"] / stats["count"]
|
|
3281
|
+
if stats["quality_count"] > 0:
|
|
3282
|
+
stats["avg_quality"] = stats["avg_quality"] / stats["quality_count"]
|
|
3283
|
+
else:
|
|
3284
|
+
stats["avg_quality"] = 0.5 # Default
|
|
3285
|
+
|
|
3286
|
+
# Select best approach (balance success rate, quality, speed)
|
|
3287
|
+
best_approach = max(
|
|
3288
|
+
approach_stats.items(),
|
|
3289
|
+
key=lambda x: (
|
|
3290
|
+
x[1]["count"] / len(similar_experiences), # Success rate
|
|
3291
|
+
x[1]["avg_quality"], # Quality
|
|
3292
|
+
-x[1]["avg_time"], # Speed (negative for faster is better)
|
|
3293
|
+
),
|
|
3294
|
+
)
|
|
3295
|
+
|
|
3296
|
+
approach_name, stats = best_approach
|
|
3297
|
+
confidence = min(1.0, stats["count"] / max(5, len(similar_experiences)))
|
|
3298
|
+
|
|
3299
|
+
return {
|
|
3300
|
+
"approach": approach_name,
|
|
3301
|
+
"confidence": confidence,
|
|
3302
|
+
"reasoning": (
|
|
3303
|
+
f"Based on {stats['count']} successful experiences with {task_type} tasks. " f"Average execution time: {stats['avg_time']:.2f}s, " f"Average quality: {stats['avg_quality']:.2f}"
|
|
3304
|
+
),
|
|
3305
|
+
"stats": stats,
|
|
3306
|
+
}
|
|
3307
|
+
|
|
3308
|
+
async def get_learning_insights(self) -> Dict[str, Any]:
|
|
3309
|
+
"""
|
|
3310
|
+
Get learning insights and analytics.
|
|
3311
|
+
|
|
3312
|
+
Provides analytics about agent learning including success rates,
|
|
3313
|
+
common patterns, and areas for improvement.
|
|
3314
|
+
|
|
3315
|
+
Returns:
|
|
3316
|
+
Dict with learning insights and statistics
|
|
3317
|
+
|
|
3318
|
+
Example:
|
|
3319
|
+
```python
|
|
3320
|
+
insights = await agent.get_learning_insights()
|
|
3321
|
+
print(f"Total experiences: {insights['total_experiences']}")
|
|
3322
|
+
print(f"Success rate: {insights['overall_success_rate']:.2%}")
|
|
3323
|
+
print(f"Most common task: {insights['most_common_task_type']}")
|
|
3324
|
+
```
|
|
3325
|
+
"""
|
|
3326
|
+
if not self._learning_enabled or not self._experiences:
|
|
3327
|
+
return {
|
|
3328
|
+
"total_experiences": 0,
|
|
3329
|
+
"learning_enabled": self._learning_enabled,
|
|
3330
|
+
}
|
|
3331
|
+
|
|
3332
|
+
total = len(self._experiences)
|
|
3333
|
+
successful = sum(1 for exp in self._experiences if exp.success)
|
|
3334
|
+
failed = total - successful
|
|
3335
|
+
|
|
3336
|
+
# Task type distribution
|
|
3337
|
+
task_types: Dict[str, int] = {}
|
|
3338
|
+
for exp in self._experiences:
|
|
3339
|
+
task_types[exp.task_type] = task_types.get(exp.task_type, 0) + 1
|
|
3340
|
+
|
|
3341
|
+
# Approach effectiveness
|
|
3342
|
+
approach_success: Dict[str, Dict[str, int]] = {}
|
|
3343
|
+
for exp in self._experiences:
|
|
3344
|
+
if exp.approach not in approach_success:
|
|
3345
|
+
approach_success[exp.approach] = {"success": 0, "failure": 0}
|
|
3346
|
+
if exp.success:
|
|
3347
|
+
approach_success[exp.approach]["success"] += 1
|
|
3348
|
+
else:
|
|
3349
|
+
approach_success[exp.approach]["failure"] += 1
|
|
3350
|
+
|
|
3351
|
+
# Calculate success rates
|
|
3352
|
+
approach_rates = {approach: stats["success"] / (stats["success"] + stats["failure"]) for approach, stats in approach_success.items()}
|
|
3353
|
+
|
|
3354
|
+
# Error patterns
|
|
3355
|
+
error_types: Dict[str, int] = {}
|
|
3356
|
+
for exp in self._experiences:
|
|
3357
|
+
if not exp.success and exp.error_type:
|
|
3358
|
+
error_types[exp.error_type] = error_types.get(exp.error_type, 0) + 1
|
|
3359
|
+
|
|
3360
|
+
return {
|
|
3361
|
+
"total_experiences": total,
|
|
3362
|
+
"successful_experiences": successful,
|
|
3363
|
+
"failed_experiences": failed,
|
|
3364
|
+
"overall_success_rate": successful / total if total > 0 else 0.0,
|
|
3365
|
+
"task_type_distribution": task_types,
|
|
3366
|
+
"most_common_task_type": (max(task_types.items(), key=lambda x: x[1])[0] if task_types else None),
|
|
3367
|
+
"approach_effectiveness": approach_rates,
|
|
3368
|
+
"best_approach": (max(approach_rates.items(), key=lambda x: x[1])[0] if approach_rates else None),
|
|
3369
|
+
"error_patterns": error_types,
|
|
3370
|
+
"most_common_error": (max(error_types.items(), key=lambda x: x[1])[0] if error_types else None),
|
|
3371
|
+
"learning_enabled": self._learning_enabled,
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
async def adapt_strategy(self, task: Dict[str, Any]) -> Dict[str, Any]:
|
|
3375
|
+
"""
|
|
3376
|
+
Adapt strategy based on learning insights.
|
|
3377
|
+
|
|
3378
|
+
Analyzes past experiences to suggest strategy adaptations for
|
|
3379
|
+
the current task.
|
|
3380
|
+
|
|
3381
|
+
Args:
|
|
3382
|
+
task: Task specification
|
|
3383
|
+
|
|
3384
|
+
Returns:
|
|
3385
|
+
Dict with strategy adaptations and recommendations
|
|
3386
|
+
|
|
3387
|
+
Example:
|
|
3388
|
+
```python
|
|
3389
|
+
adaptations = await agent.adapt_strategy(
|
|
3390
|
+
task={"description": "Complex analysis", "type": "analysis"}
|
|
3391
|
+
)
|
|
3392
|
+
print(f"Recommended approach: {adaptations['recommended_approach']}")
|
|
3393
|
+
print(f"Suggested tools: {adaptations['suggested_tools']}")
|
|
3394
|
+
```
|
|
3395
|
+
"""
|
|
3396
|
+
if not self._learning_enabled:
|
|
3397
|
+
return {"adapted": False, "reason": "Learning not enabled"}
|
|
3398
|
+
|
|
3399
|
+
# Get recommended approach
|
|
3400
|
+
recommendation = await self.get_recommended_approach(task)
|
|
3401
|
+
|
|
3402
|
+
if not recommendation:
|
|
3403
|
+
return {
|
|
3404
|
+
"adapted": False,
|
|
3405
|
+
"reason": "No relevant experiences found",
|
|
3406
|
+
}
|
|
3407
|
+
|
|
3408
|
+
# Classify task
|
|
3409
|
+
task_type = await self._classify_task(task)
|
|
3410
|
+
|
|
3411
|
+
# Find similar successful experiences
|
|
3412
|
+
similar_successful = [exp for exp in self._experiences if exp.task_type == task_type and exp.success]
|
|
3413
|
+
|
|
3414
|
+
# Analyze tool usage patterns
|
|
3415
|
+
tool_usage: Dict[str, int] = {}
|
|
3416
|
+
for exp in similar_successful:
|
|
3417
|
+
for tool in exp.tools_used:
|
|
3418
|
+
tool_usage[tool] = tool_usage.get(tool, 0) + 1
|
|
3419
|
+
|
|
3420
|
+
# Get most commonly used tools
|
|
3421
|
+
suggested_tools = sorted(tool_usage.items(), key=lambda x: x[1], reverse=True)[:5] # Top 5 tools
|
|
3422
|
+
|
|
3423
|
+
return {
|
|
3424
|
+
"adapted": True,
|
|
3425
|
+
"recommended_approach": recommendation["approach"],
|
|
3426
|
+
"confidence": recommendation["confidence"],
|
|
3427
|
+
"reasoning": recommendation["reasoning"],
|
|
3428
|
+
"suggested_tools": [tool for tool, _ in suggested_tools],
|
|
3429
|
+
"tool_usage_stats": dict(suggested_tools),
|
|
3430
|
+
"based_on_experiences": len(similar_successful),
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
async def _classify_task(self, task: Dict[str, Any]) -> str:
|
|
3434
|
+
"""
|
|
3435
|
+
Classify task into a type/category.
|
|
3436
|
+
|
|
3437
|
+
Uses simple heuristics to classify tasks. Can be overridden by
|
|
3438
|
+
subclasses for more sophisticated classification.
|
|
3439
|
+
|
|
3440
|
+
Args:
|
|
3441
|
+
task: Task specification
|
|
3442
|
+
|
|
3443
|
+
Returns:
|
|
3444
|
+
Task type string
|
|
3445
|
+
|
|
3446
|
+
Example:
|
|
3447
|
+
```python
|
|
3448
|
+
task_type = await agent._classify_task(
|
|
3449
|
+
{"description": "Analyze sales data"}
|
|
3450
|
+
)
|
|
3451
|
+
# Returns: "analysis"
|
|
3452
|
+
```
|
|
3453
|
+
"""
|
|
3454
|
+
# Check explicit type
|
|
3455
|
+
if "type" in task:
|
|
3456
|
+
return task["type"]
|
|
3457
|
+
|
|
3458
|
+
# Simple keyword-based classification
|
|
3459
|
+
description = task.get("description", "").lower()
|
|
3460
|
+
|
|
3461
|
+
if any(word in description for word in ["analyze", "analysis", "examine"]):
|
|
3462
|
+
return "analysis"
|
|
3463
|
+
elif any(word in description for word in ["search", "find", "lookup"]):
|
|
3464
|
+
return "search"
|
|
3465
|
+
elif any(word in description for word in ["create", "generate", "write"]):
|
|
3466
|
+
return "generation"
|
|
3467
|
+
elif any(word in description for word in ["summarize", "summary"]):
|
|
3468
|
+
return "summarization"
|
|
3469
|
+
elif any(word in description for word in ["calculate", "compute"]):
|
|
3470
|
+
return "calculation"
|
|
3471
|
+
elif any(word in description for word in ["translate", "convert"]):
|
|
3472
|
+
return "translation"
|
|
3473
|
+
else:
|
|
3474
|
+
return "general"
|
|
3475
|
+
|
|
3476
|
+
# ==================== Resource Management (Phase 8 - Tasks 1.16.11-1.16.17) ====================
|
|
3477
|
+
|
|
3478
|
+
async def check_resource_availability(self) -> Dict[str, Any]:
|
|
3479
|
+
"""
|
|
3480
|
+
Check if resources are available for task execution.
|
|
3481
|
+
|
|
3482
|
+
Checks against configured resource limits including:
|
|
3483
|
+
- Concurrent task limits
|
|
3484
|
+
- Token rate limits
|
|
3485
|
+
- Tool call rate limits
|
|
3486
|
+
|
|
3487
|
+
Returns:
|
|
3488
|
+
Dict with 'available' (bool) and details about resource status
|
|
3489
|
+
|
|
3490
|
+
Example:
|
|
3491
|
+
```python
|
|
3492
|
+
status = await agent.check_resource_availability()
|
|
3493
|
+
if status['available']:
|
|
3494
|
+
await agent.execute_task(task, context)
|
|
3495
|
+
else:
|
|
3496
|
+
print(f"Resources unavailable: {status['reason']}")
|
|
3497
|
+
```
|
|
3498
|
+
"""
|
|
3499
|
+
if not self._resource_limits.enforce_limits:
|
|
3500
|
+
return {"available": True, "reason": "Limits not enforced"}
|
|
3501
|
+
|
|
3502
|
+
# Check concurrent task limit
|
|
3503
|
+
if len(self._active_tasks) >= self._resource_limits.max_concurrent_tasks:
|
|
3504
|
+
return {
|
|
3505
|
+
"available": False,
|
|
3506
|
+
"reason": "Concurrent task limit reached",
|
|
3507
|
+
"active_tasks": len(self._active_tasks),
|
|
3508
|
+
"max_tasks": self._resource_limits.max_concurrent_tasks,
|
|
3509
|
+
}
|
|
3510
|
+
|
|
3511
|
+
# Check token rate limits
|
|
3512
|
+
token_check = await self._check_token_rate_limit()
|
|
3513
|
+
if not token_check["available"]:
|
|
3514
|
+
return token_check
|
|
3515
|
+
|
|
3516
|
+
# Check tool call rate limits
|
|
3517
|
+
tool_check = await self._check_tool_call_rate_limit()
|
|
3518
|
+
if not tool_check["available"]:
|
|
3519
|
+
return tool_check
|
|
3520
|
+
|
|
3521
|
+
return {
|
|
3522
|
+
"available": True,
|
|
3523
|
+
"active_tasks": len(self._active_tasks),
|
|
3524
|
+
"max_tasks": self._resource_limits.max_concurrent_tasks,
|
|
3525
|
+
}
|
|
3526
|
+
|
|
3527
|
+
async def wait_for_resources(self, timeout: Optional[float] = None) -> bool:
|
|
3528
|
+
"""
|
|
3529
|
+
Wait for resources to become available.
|
|
3530
|
+
|
|
3531
|
+
Args:
|
|
3532
|
+
timeout: Maximum time to wait in seconds (uses resource_wait_timeout_seconds if None)
|
|
3533
|
+
|
|
3534
|
+
Returns:
|
|
3535
|
+
True if resources became available, False if timeout
|
|
3536
|
+
|
|
3537
|
+
Example:
|
|
3538
|
+
```python
|
|
3539
|
+
if await agent.wait_for_resources(timeout=30):
|
|
3540
|
+
await agent.execute_task(task, context)
|
|
3541
|
+
else:
|
|
3542
|
+
print("Timeout waiting for resources")
|
|
3543
|
+
```
|
|
3544
|
+
"""
|
|
3545
|
+
if timeout is None:
|
|
3546
|
+
timeout = self._resource_limits.resource_wait_timeout_seconds
|
|
3547
|
+
|
|
3548
|
+
start_time = time.time()
|
|
3549
|
+
check_interval = 0.5 # Check every 500ms
|
|
3550
|
+
|
|
3551
|
+
while time.time() - start_time < timeout:
|
|
3552
|
+
status = await self.check_resource_availability()
|
|
3553
|
+
if status["available"]:
|
|
3554
|
+
return True
|
|
3555
|
+
|
|
3556
|
+
# Wait before next check
|
|
3557
|
+
await asyncio.sleep(check_interval)
|
|
3558
|
+
|
|
3559
|
+
logger.warning(f"Timeout waiting for resources after {timeout}s")
|
|
3560
|
+
return False
|
|
3561
|
+
|
|
3562
|
+
async def get_resource_usage(self) -> Dict[str, Any]:
|
|
3563
|
+
"""
|
|
3564
|
+
Get current resource usage statistics.
|
|
3565
|
+
|
|
3566
|
+
Returns:
|
|
3567
|
+
Dict with resource usage information
|
|
3568
|
+
|
|
3569
|
+
Example:
|
|
3570
|
+
```python
|
|
3571
|
+
usage = await agent.get_resource_usage()
|
|
3572
|
+
print(f"Active tasks: {usage['active_tasks']}")
|
|
3573
|
+
print(f"Tokens/min: {usage['tokens_per_minute']}")
|
|
3574
|
+
print(f"Tool calls/min: {usage['tool_calls_per_minute']}")
|
|
3575
|
+
```
|
|
3576
|
+
"""
|
|
3577
|
+
current_time = time.time()
|
|
3578
|
+
|
|
3579
|
+
# Calculate token usage rates
|
|
3580
|
+
tokens_last_minute = sum(count for ts, count in self._token_usage_window if current_time - ts < 60)
|
|
3581
|
+
tokens_last_hour = sum(count for ts, count in self._token_usage_window if current_time - ts < 3600)
|
|
3582
|
+
|
|
3583
|
+
# Calculate tool call rates
|
|
3584
|
+
tool_calls_last_minute = sum(1 for ts in self._tool_call_window if current_time - ts < 60)
|
|
3585
|
+
tool_calls_last_hour = sum(1 for ts in self._tool_call_window if current_time - ts < 3600)
|
|
3586
|
+
|
|
3587
|
+
return {
|
|
3588
|
+
"active_tasks": len(self._active_tasks),
|
|
3589
|
+
"max_concurrent_tasks": self._resource_limits.max_concurrent_tasks,
|
|
3590
|
+
"task_utilization": len(self._active_tasks) / self._resource_limits.max_concurrent_tasks,
|
|
3591
|
+
"tokens_per_minute": tokens_last_minute,
|
|
3592
|
+
"tokens_per_hour": tokens_last_hour,
|
|
3593
|
+
"max_tokens_per_minute": self._resource_limits.max_tokens_per_minute,
|
|
3594
|
+
"max_tokens_per_hour": self._resource_limits.max_tokens_per_hour,
|
|
3595
|
+
"tool_calls_per_minute": tool_calls_last_minute,
|
|
3596
|
+
"tool_calls_per_hour": tool_calls_last_hour,
|
|
3597
|
+
"max_tool_calls_per_minute": self._resource_limits.max_tool_calls_per_minute,
|
|
3598
|
+
"max_tool_calls_per_hour": self._resource_limits.max_tool_calls_per_hour,
|
|
3599
|
+
"limits_enforced": self._resource_limits.enforce_limits,
|
|
3600
|
+
}
|
|
3601
|
+
|
|
3602
|
+
async def _check_token_rate_limit(self) -> Dict[str, Any]:
|
|
3603
|
+
"""
|
|
3604
|
+
Check token rate limits.
|
|
3605
|
+
|
|
3606
|
+
Returns:
|
|
3607
|
+
Dict with 'available' (bool) and limit details
|
|
3608
|
+
"""
|
|
3609
|
+
if not self._resource_limits.enforce_limits:
|
|
3610
|
+
return {"available": True}
|
|
3611
|
+
|
|
3612
|
+
current_time = time.time()
|
|
3613
|
+
|
|
3614
|
+
# Clean old entries (older than 1 hour)
|
|
3615
|
+
self._token_usage_window = [(ts, count) for ts, count in self._token_usage_window if current_time - ts < 3600]
|
|
3616
|
+
|
|
3617
|
+
# Check per-minute limit
|
|
3618
|
+
if self._resource_limits.max_tokens_per_minute is not None:
|
|
3619
|
+
tokens_last_minute = sum(count for ts, count in self._token_usage_window if current_time - ts < 60)
|
|
3620
|
+
if tokens_last_minute >= self._resource_limits.max_tokens_per_minute:
|
|
3621
|
+
return {
|
|
3622
|
+
"available": False,
|
|
3623
|
+
"reason": "Token rate limit (per minute) reached",
|
|
3624
|
+
"tokens_used": tokens_last_minute,
|
|
3625
|
+
"limit": self._resource_limits.max_tokens_per_minute,
|
|
3626
|
+
"window": "minute",
|
|
3627
|
+
}
|
|
3628
|
+
|
|
3629
|
+
# Check per-hour limit
|
|
3630
|
+
if self._resource_limits.max_tokens_per_hour is not None:
|
|
3631
|
+
tokens_last_hour = sum(count for ts, count in self._token_usage_window)
|
|
3632
|
+
if tokens_last_hour >= self._resource_limits.max_tokens_per_hour:
|
|
3633
|
+
return {
|
|
3634
|
+
"available": False,
|
|
3635
|
+
"reason": "Token rate limit (per hour) reached",
|
|
3636
|
+
"tokens_used": tokens_last_hour,
|
|
3637
|
+
"limit": self._resource_limits.max_tokens_per_hour,
|
|
3638
|
+
"window": "hour",
|
|
3639
|
+
}
|
|
3640
|
+
|
|
3641
|
+
return {"available": True}
|
|
3642
|
+
|
|
3643
|
+
async def _check_tool_call_rate_limit(self) -> Dict[str, Any]:
|
|
3644
|
+
"""
|
|
3645
|
+
Check tool call rate limits.
|
|
3646
|
+
|
|
3647
|
+
Returns:
|
|
3648
|
+
Dict with 'available' (bool) and limit details
|
|
3649
|
+
"""
|
|
3650
|
+
if not self._resource_limits.enforce_limits:
|
|
3651
|
+
return {"available": True}
|
|
3652
|
+
|
|
3653
|
+
current_time = time.time()
|
|
3654
|
+
|
|
3655
|
+
# Clean old entries (older than 1 hour)
|
|
3656
|
+
self._tool_call_window = [ts for ts in self._tool_call_window if current_time - ts < 3600]
|
|
3657
|
+
|
|
3658
|
+
# Check per-minute limit
|
|
3659
|
+
if self._resource_limits.max_tool_calls_per_minute is not None:
|
|
3660
|
+
calls_last_minute = sum(1 for ts in self._tool_call_window if current_time - ts < 60)
|
|
3661
|
+
if calls_last_minute >= self._resource_limits.max_tool_calls_per_minute:
|
|
3662
|
+
return {
|
|
3663
|
+
"available": False,
|
|
3664
|
+
"reason": "Tool call rate limit (per minute) reached",
|
|
3665
|
+
"calls_made": calls_last_minute,
|
|
3666
|
+
"limit": self._resource_limits.max_tool_calls_per_minute,
|
|
3667
|
+
"window": "minute",
|
|
3668
|
+
}
|
|
3669
|
+
|
|
3670
|
+
# Check per-hour limit
|
|
3671
|
+
if self._resource_limits.max_tool_calls_per_hour is not None:
|
|
3672
|
+
calls_last_hour = len(self._tool_call_window)
|
|
3673
|
+
if calls_last_hour >= self._resource_limits.max_tool_calls_per_hour:
|
|
3674
|
+
return {
|
|
3675
|
+
"available": False,
|
|
3676
|
+
"reason": "Tool call rate limit (per hour) reached",
|
|
3677
|
+
"calls_made": calls_last_hour,
|
|
3678
|
+
"limit": self._resource_limits.max_tool_calls_per_hour,
|
|
3679
|
+
"window": "hour",
|
|
3680
|
+
}
|
|
3681
|
+
|
|
3682
|
+
return {"available": True}
|
|
3683
|
+
|
|
3684
|
+
# ==================== Error Recovery (Phase 8 - Tasks 1.16.18-1.16.22) ====================
|
|
3685
|
+
|
|
3686
|
+
async def execute_with_recovery(
|
|
3687
|
+
self,
|
|
3688
|
+
task: Dict[str, Any],
|
|
3689
|
+
context: Dict[str, Any],
|
|
3690
|
+
strategies: Optional[List[str]] = None,
|
|
3691
|
+
) -> Dict[str, Any]:
|
|
3692
|
+
"""
|
|
3693
|
+
Execute task with advanced error recovery strategies.
|
|
3694
|
+
|
|
3695
|
+
Tries multiple recovery strategies in sequence until one succeeds:
|
|
3696
|
+
1. Retry with exponential backoff
|
|
3697
|
+
2. Simplify task and retry
|
|
3698
|
+
3. Use fallback approach
|
|
3699
|
+
4. Delegate to another agent
|
|
3700
|
+
|
|
3701
|
+
Args:
|
|
3702
|
+
task: Task specification
|
|
3703
|
+
context: Execution context
|
|
3704
|
+
strategies: List of strategy names to try (uses default chain if None)
|
|
3705
|
+
|
|
3706
|
+
Returns:
|
|
3707
|
+
Task execution result
|
|
3708
|
+
|
|
3709
|
+
Raises:
|
|
3710
|
+
TaskExecutionError: If all recovery strategies fail
|
|
3711
|
+
|
|
3712
|
+
Example:
|
|
3713
|
+
```python
|
|
3714
|
+
result = await agent.execute_with_recovery(
|
|
3715
|
+
task={"description": "Complex analysis"},
|
|
3716
|
+
context={},
|
|
3717
|
+
strategies=["retry", "simplify", "delegate"]
|
|
3718
|
+
)
|
|
3719
|
+
```
|
|
3720
|
+
"""
|
|
3721
|
+
from .models import RecoveryStrategy
|
|
3722
|
+
from .exceptions import TaskExecutionError
|
|
3723
|
+
|
|
3724
|
+
# Default strategy chain
|
|
3725
|
+
if strategies is None:
|
|
3726
|
+
strategies = [
|
|
3727
|
+
RecoveryStrategy.RETRY,
|
|
3728
|
+
RecoveryStrategy.SIMPLIFY,
|
|
3729
|
+
RecoveryStrategy.FALLBACK,
|
|
3730
|
+
RecoveryStrategy.DELEGATE,
|
|
3731
|
+
]
|
|
3732
|
+
|
|
3733
|
+
errors = []
|
|
3734
|
+
|
|
3735
|
+
for strategy in strategies:
|
|
3736
|
+
try:
|
|
3737
|
+
logger.info(f"Attempting recovery strategy: {strategy}")
|
|
3738
|
+
|
|
3739
|
+
if strategy == RecoveryStrategy.RETRY:
|
|
3740
|
+
# Retry with exponential backoff (using existing retry mechanism)
|
|
3741
|
+
result = await self._execute_with_retry(self.execute_task, task, context)
|
|
3742
|
+
logger.info(f"Recovery successful with strategy: {strategy}")
|
|
3743
|
+
return result
|
|
3744
|
+
|
|
3745
|
+
elif strategy == RecoveryStrategy.SIMPLIFY:
|
|
3746
|
+
# Simplify task and retry
|
|
3747
|
+
simplified_task = await self._simplify_task(task)
|
|
3748
|
+
result = await self.execute_task(simplified_task, context)
|
|
3749
|
+
logger.info(f"Recovery successful with strategy: {strategy}")
|
|
3750
|
+
return result
|
|
3751
|
+
|
|
3752
|
+
elif strategy == RecoveryStrategy.FALLBACK:
|
|
3753
|
+
# Use fallback approach
|
|
3754
|
+
result = await self._execute_with_fallback(task, context)
|
|
3755
|
+
logger.info(f"Recovery successful with strategy: {strategy}")
|
|
3756
|
+
return result
|
|
3757
|
+
|
|
3758
|
+
elif strategy == RecoveryStrategy.DELEGATE:
|
|
3759
|
+
# Delegate to another agent
|
|
3760
|
+
if self._collaboration_enabled:
|
|
3761
|
+
result = await self._delegate_to_capable_agent(task, context)
|
|
3762
|
+
logger.info(f"Recovery successful with strategy: {strategy}")
|
|
3763
|
+
return result
|
|
3764
|
+
else:
|
|
3765
|
+
logger.warning("Delegation not available (collaboration disabled)")
|
|
3766
|
+
continue
|
|
3767
|
+
|
|
3768
|
+
except Exception as e:
|
|
3769
|
+
logger.warning(f"Recovery strategy {strategy} failed: {e}")
|
|
3770
|
+
errors.append({"strategy": strategy, "error": str(e)})
|
|
3771
|
+
continue
|
|
3772
|
+
|
|
3773
|
+
# All strategies failed
|
|
3774
|
+
error_summary = "; ".join([f"{e['strategy']}: {e['error']}" for e in errors])
|
|
3775
|
+
raise TaskExecutionError(
|
|
3776
|
+
f"All recovery strategies failed. Errors: {error_summary}",
|
|
3777
|
+
agent_id=self.agent_id,
|
|
3778
|
+
task_id=task.get("task_id"),
|
|
3779
|
+
)
|
|
3780
|
+
|
|
3781
|
+
async def _simplify_task(self, task: Dict[str, Any]) -> Dict[str, Any]:
|
|
3782
|
+
"""
|
|
3783
|
+
Simplify a task to make it easier to execute.
|
|
3784
|
+
|
|
3785
|
+
Strategies:
|
|
3786
|
+
- Reduce complexity by breaking into smaller parts
|
|
3787
|
+
- Remove optional requirements
|
|
3788
|
+
- Use simpler language
|
|
3789
|
+
|
|
3790
|
+
Args:
|
|
3791
|
+
task: Original task specification
|
|
3792
|
+
|
|
3793
|
+
Returns:
|
|
3794
|
+
Simplified task specification
|
|
3795
|
+
|
|
3796
|
+
Example:
|
|
3797
|
+
```python
|
|
3798
|
+
simplified = await agent._simplify_task(
|
|
3799
|
+
{"description": "Perform comprehensive analysis with visualizations"}
|
|
3800
|
+
)
|
|
3801
|
+
# Returns: {"description": "Perform basic analysis"}
|
|
3802
|
+
```
|
|
3803
|
+
"""
|
|
3804
|
+
description = task.get("description", "")
|
|
3805
|
+
|
|
3806
|
+
# Simple heuristics for simplification
|
|
3807
|
+
simplified_description = description
|
|
3808
|
+
|
|
3809
|
+
# Remove complexity keywords
|
|
3810
|
+
complexity_words = [
|
|
3811
|
+
"comprehensive",
|
|
3812
|
+
"detailed",
|
|
3813
|
+
"thorough",
|
|
3814
|
+
"extensive",
|
|
3815
|
+
"in-depth",
|
|
3816
|
+
"complete",
|
|
3817
|
+
"full",
|
|
3818
|
+
"exhaustive",
|
|
3819
|
+
]
|
|
3820
|
+
for word in complexity_words:
|
|
3821
|
+
simplified_description = simplified_description.replace(word, "basic")
|
|
3822
|
+
|
|
3823
|
+
# Remove optional requirements
|
|
3824
|
+
optional_phrases = [
|
|
3825
|
+
"with visualizations",
|
|
3826
|
+
"with charts",
|
|
3827
|
+
"with graphs",
|
|
3828
|
+
"with examples",
|
|
3829
|
+
"with details",
|
|
3830
|
+
"with explanations",
|
|
3831
|
+
]
|
|
3832
|
+
for phrase in optional_phrases:
|
|
3833
|
+
simplified_description = simplified_description.replace(phrase, "")
|
|
3834
|
+
|
|
3835
|
+
# Clean up extra spaces
|
|
3836
|
+
simplified_description = " ".join(simplified_description.split())
|
|
3837
|
+
|
|
3838
|
+
simplified_task = task.copy()
|
|
3839
|
+
simplified_task["description"] = simplified_description
|
|
3840
|
+
simplified_task["simplified"] = True
|
|
3841
|
+
simplified_task["original_description"] = description
|
|
3842
|
+
|
|
3843
|
+
logger.debug(f"Simplified task: '{description}' -> '{simplified_description}'")
|
|
3844
|
+
|
|
3845
|
+
return simplified_task
|
|
3846
|
+
|
|
3847
|
+
async def _execute_with_fallback(self, task: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
|
|
3848
|
+
"""
|
|
3849
|
+
Execute task with fallback approach.
|
|
3850
|
+
|
|
3851
|
+
Uses a simpler, more reliable approach when the primary approach fails.
|
|
3852
|
+
|
|
3853
|
+
Args:
|
|
3854
|
+
task: Task specification
|
|
3855
|
+
context: Execution context
|
|
3856
|
+
|
|
3857
|
+
Returns:
|
|
3858
|
+
Task execution result
|
|
3859
|
+
|
|
3860
|
+
Example:
|
|
3861
|
+
```python
|
|
3862
|
+
result = await agent._execute_with_fallback(task, context)
|
|
3863
|
+
```
|
|
3864
|
+
"""
|
|
3865
|
+
# Create fallback task with reduced requirements
|
|
3866
|
+
fallback_task = task.copy()
|
|
3867
|
+
fallback_task["fallback_mode"] = True
|
|
3868
|
+
|
|
3869
|
+
# Reduce max_tokens if specified
|
|
3870
|
+
if "max_tokens" in context:
|
|
3871
|
+
context = context.copy()
|
|
3872
|
+
context["max_tokens"] = min(context["max_tokens"], 1000)
|
|
3873
|
+
|
|
3874
|
+
# Reduce temperature for more deterministic output
|
|
3875
|
+
if "temperature" in context:
|
|
3876
|
+
context = context.copy()
|
|
3877
|
+
context["temperature"] = 0.3
|
|
3878
|
+
|
|
3879
|
+
logger.info("Executing with fallback approach (reduced requirements)")
|
|
3880
|
+
|
|
3881
|
+
# Execute with modified parameters
|
|
3882
|
+
result = await self.execute_task(fallback_task, context)
|
|
3883
|
+
result["fallback_used"] = True
|
|
3884
|
+
|
|
3885
|
+
return result
|
|
3886
|
+
|
|
3887
|
+
async def _delegate_to_capable_agent(self, task: Dict[str, Any], context: Dict[str, Any]) -> Dict[str, Any]:
|
|
3888
|
+
"""
|
|
3889
|
+
Delegate task to a capable agent as recovery strategy.
|
|
3890
|
+
|
|
3891
|
+
Finds an agent capable of handling the task and delegates to it.
|
|
3892
|
+
|
|
3893
|
+
Args:
|
|
3894
|
+
task: Task specification
|
|
3895
|
+
context: Execution context
|
|
3896
|
+
|
|
3897
|
+
Returns:
|
|
3898
|
+
Task execution result from delegated agent
|
|
3899
|
+
|
|
3900
|
+
Raises:
|
|
3901
|
+
ValueError: If no capable agent found
|
|
3902
|
+
|
|
3903
|
+
Example:
|
|
3904
|
+
```python
|
|
3905
|
+
result = await agent._delegate_to_capable_agent(task, context)
|
|
3906
|
+
```
|
|
3907
|
+
"""
|
|
3908
|
+
if not self._collaboration_enabled:
|
|
3909
|
+
raise ValueError("Collaboration not enabled, cannot delegate")
|
|
3910
|
+
|
|
3911
|
+
# Try to classify task and find capable agents
|
|
3912
|
+
task_type = await self._classify_task(task)
|
|
3913
|
+
|
|
3914
|
+
# Look for agents with matching capabilities
|
|
3915
|
+
capable_agents = []
|
|
3916
|
+
for agent_id, agent in self._agent_registry.items():
|
|
3917
|
+
if agent_id == self.agent_id:
|
|
3918
|
+
continue # Skip self
|
|
3919
|
+
|
|
3920
|
+
# Check if agent has relevant capabilities
|
|
3921
|
+
agent_capabilities = getattr(agent, "capabilities", [])
|
|
3922
|
+
if task_type in agent_capabilities or "general" in agent_capabilities:
|
|
3923
|
+
capable_agents.append(agent)
|
|
3924
|
+
|
|
3925
|
+
if not capable_agents:
|
|
3926
|
+
# Try any available agent as last resort
|
|
3927
|
+
capable_agents = [agent for agent_id, agent in self._agent_registry.items() if agent_id != self.agent_id]
|
|
3928
|
+
|
|
3929
|
+
if not capable_agents:
|
|
3930
|
+
raise ValueError("No capable agents available for delegation")
|
|
3931
|
+
|
|
3932
|
+
# Delegate to first capable agent
|
|
3933
|
+
target_agent = capable_agents[0]
|
|
3934
|
+
logger.info(f"Delegating task to {target_agent.agent_id} for recovery")
|
|
3935
|
+
|
|
3936
|
+
result = await target_agent.execute_task(task, context={**context, "delegated_by": self.agent_id, "recovery_delegation": True})
|
|
3937
|
+
|
|
3938
|
+
result["delegated_to"] = target_agent.agent_id
|
|
3939
|
+
result["recovery_delegation"] = True
|
|
3940
|
+
|
|
3941
|
+
return result
|
|
3942
|
+
|
|
3943
|
+
def __str__(self) -> str:
|
|
3944
|
+
"""String representation."""
|
|
3945
|
+
return f"Agent({self.agent_id}, {self.name}, {self.agent_type.value}, {self._state.value})"
|
|
3946
|
+
|
|
3947
|
+
def __repr__(self) -> str:
|
|
3948
|
+
"""Detailed representation."""
|
|
3949
|
+
return f"BaseAIAgent(agent_id='{self.agent_id}', name='{self.name}', " f"type='{self.agent_type.value}', state='{self._state.value}')"
|