aiecs 1.5.1__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.
- aiecs/__init__.py +72 -0
- aiecs/__main__.py +41 -0
- aiecs/aiecs_client.py +469 -0
- aiecs/application/__init__.py +10 -0
- aiecs/application/executors/__init__.py +10 -0
- aiecs/application/executors/operation_executor.py +363 -0
- aiecs/application/knowledge_graph/__init__.py +7 -0
- aiecs/application/knowledge_graph/builder/__init__.py +37 -0
- aiecs/application/knowledge_graph/builder/document_builder.py +375 -0
- aiecs/application/knowledge_graph/builder/graph_builder.py +356 -0
- aiecs/application/knowledge_graph/builder/schema_mapping.py +531 -0
- aiecs/application/knowledge_graph/builder/structured_pipeline.py +443 -0
- aiecs/application/knowledge_graph/builder/text_chunker.py +319 -0
- aiecs/application/knowledge_graph/extractors/__init__.py +27 -0
- aiecs/application/knowledge_graph/extractors/base.py +100 -0
- aiecs/application/knowledge_graph/extractors/llm_entity_extractor.py +327 -0
- aiecs/application/knowledge_graph/extractors/llm_relation_extractor.py +349 -0
- aiecs/application/knowledge_graph/extractors/ner_entity_extractor.py +244 -0
- aiecs/application/knowledge_graph/fusion/__init__.py +23 -0
- aiecs/application/knowledge_graph/fusion/entity_deduplicator.py +387 -0
- aiecs/application/knowledge_graph/fusion/entity_linker.py +343 -0
- aiecs/application/knowledge_graph/fusion/knowledge_fusion.py +580 -0
- aiecs/application/knowledge_graph/fusion/relation_deduplicator.py +189 -0
- aiecs/application/knowledge_graph/pattern_matching/__init__.py +21 -0
- aiecs/application/knowledge_graph/pattern_matching/pattern_matcher.py +344 -0
- aiecs/application/knowledge_graph/pattern_matching/query_executor.py +378 -0
- aiecs/application/knowledge_graph/profiling/__init__.py +12 -0
- aiecs/application/knowledge_graph/profiling/query_plan_visualizer.py +199 -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 +347 -0
- aiecs/application/knowledge_graph/reasoning/inference_engine.py +504 -0
- aiecs/application/knowledge_graph/reasoning/logic_form_parser.py +167 -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 +630 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/ast_validator.py +654 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/error_handler.py +477 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/parser.py +390 -0
- aiecs/application/knowledge_graph/reasoning/logic_parser/query_context.py +217 -0
- aiecs/application/knowledge_graph/reasoning/logic_query_integration.py +169 -0
- aiecs/application/knowledge_graph/reasoning/query_planner.py +872 -0
- aiecs/application/knowledge_graph/reasoning/reasoning_engine.py +554 -0
- aiecs/application/knowledge_graph/retrieval/__init__.py +19 -0
- aiecs/application/knowledge_graph/retrieval/retrieval_strategies.py +596 -0
- aiecs/application/knowledge_graph/search/__init__.py +59 -0
- aiecs/application/knowledge_graph/search/hybrid_search.py +423 -0
- aiecs/application/knowledge_graph/search/reranker.py +295 -0
- aiecs/application/knowledge_graph/search/reranker_strategies.py +553 -0
- aiecs/application/knowledge_graph/search/text_similarity.py +398 -0
- aiecs/application/knowledge_graph/traversal/__init__.py +15 -0
- aiecs/application/knowledge_graph/traversal/enhanced_traversal.py +329 -0
- aiecs/application/knowledge_graph/traversal/path_scorer.py +269 -0
- aiecs/application/knowledge_graph/validators/__init__.py +13 -0
- aiecs/application/knowledge_graph/validators/relation_validator.py +189 -0
- aiecs/application/knowledge_graph/visualization/__init__.py +11 -0
- aiecs/application/knowledge_graph/visualization/graph_visualizer.py +321 -0
- aiecs/common/__init__.py +9 -0
- aiecs/common/knowledge_graph/__init__.py +17 -0
- aiecs/common/knowledge_graph/runnable.py +484 -0
- aiecs/config/__init__.py +16 -0
- aiecs/config/config.py +498 -0
- aiecs/config/graph_config.py +137 -0
- aiecs/config/registry.py +23 -0
- aiecs/core/__init__.py +46 -0
- aiecs/core/interface/__init__.py +34 -0
- aiecs/core/interface/execution_interface.py +152 -0
- aiecs/core/interface/storage_interface.py +171 -0
- aiecs/domain/__init__.py +289 -0
- aiecs/domain/agent/__init__.py +189 -0
- aiecs/domain/agent/base_agent.py +697 -0
- aiecs/domain/agent/exceptions.py +103 -0
- aiecs/domain/agent/graph_aware_mixin.py +559 -0
- aiecs/domain/agent/hybrid_agent.py +490 -0
- aiecs/domain/agent/integration/__init__.py +26 -0
- aiecs/domain/agent/integration/context_compressor.py +222 -0
- aiecs/domain/agent/integration/context_engine_adapter.py +252 -0
- aiecs/domain/agent/integration/retry_policy.py +219 -0
- aiecs/domain/agent/integration/role_config.py +213 -0
- aiecs/domain/agent/knowledge_aware_agent.py +646 -0
- aiecs/domain/agent/lifecycle.py +296 -0
- aiecs/domain/agent/llm_agent.py +300 -0
- aiecs/domain/agent/memory/__init__.py +12 -0
- aiecs/domain/agent/memory/conversation.py +197 -0
- aiecs/domain/agent/migration/__init__.py +14 -0
- aiecs/domain/agent/migration/conversion.py +160 -0
- aiecs/domain/agent/migration/legacy_wrapper.py +90 -0
- aiecs/domain/agent/models.py +317 -0
- aiecs/domain/agent/observability.py +407 -0
- aiecs/domain/agent/persistence.py +289 -0
- aiecs/domain/agent/prompts/__init__.py +29 -0
- aiecs/domain/agent/prompts/builder.py +161 -0
- aiecs/domain/agent/prompts/formatters.py +189 -0
- aiecs/domain/agent/prompts/template.py +255 -0
- aiecs/domain/agent/registry.py +260 -0
- aiecs/domain/agent/tool_agent.py +257 -0
- aiecs/domain/agent/tools/__init__.py +12 -0
- aiecs/domain/agent/tools/schema_generator.py +221 -0
- aiecs/domain/community/__init__.py +155 -0
- aiecs/domain/community/agent_adapter.py +477 -0
- aiecs/domain/community/analytics.py +481 -0
- aiecs/domain/community/collaborative_workflow.py +642 -0
- aiecs/domain/community/communication_hub.py +645 -0
- aiecs/domain/community/community_builder.py +320 -0
- aiecs/domain/community/community_integration.py +800 -0
- aiecs/domain/community/community_manager.py +813 -0
- aiecs/domain/community/decision_engine.py +879 -0
- aiecs/domain/community/exceptions.py +225 -0
- aiecs/domain/community/models/__init__.py +33 -0
- aiecs/domain/community/models/community_models.py +268 -0
- aiecs/domain/community/resource_manager.py +457 -0
- aiecs/domain/community/shared_context_manager.py +603 -0
- aiecs/domain/context/__init__.py +58 -0
- aiecs/domain/context/context_engine.py +989 -0
- aiecs/domain/context/conversation_models.py +354 -0
- aiecs/domain/context/graph_memory.py +467 -0
- aiecs/domain/execution/__init__.py +12 -0
- aiecs/domain/execution/model.py +57 -0
- aiecs/domain/knowledge_graph/__init__.py +19 -0
- aiecs/domain/knowledge_graph/models/__init__.py +52 -0
- aiecs/domain/knowledge_graph/models/entity.py +130 -0
- aiecs/domain/knowledge_graph/models/evidence.py +194 -0
- aiecs/domain/knowledge_graph/models/inference_rule.py +186 -0
- aiecs/domain/knowledge_graph/models/path.py +179 -0
- aiecs/domain/knowledge_graph/models/path_pattern.py +173 -0
- aiecs/domain/knowledge_graph/models/query.py +272 -0
- aiecs/domain/knowledge_graph/models/query_plan.py +187 -0
- aiecs/domain/knowledge_graph/models/relation.py +136 -0
- aiecs/domain/knowledge_graph/schema/__init__.py +23 -0
- aiecs/domain/knowledge_graph/schema/entity_type.py +135 -0
- aiecs/domain/knowledge_graph/schema/graph_schema.py +271 -0
- aiecs/domain/knowledge_graph/schema/property_schema.py +155 -0
- aiecs/domain/knowledge_graph/schema/relation_type.py +171 -0
- aiecs/domain/knowledge_graph/schema/schema_manager.py +496 -0
- aiecs/domain/knowledge_graph/schema/type_enums.py +205 -0
- aiecs/domain/task/__init__.py +13 -0
- aiecs/domain/task/dsl_processor.py +613 -0
- aiecs/domain/task/model.py +62 -0
- aiecs/domain/task/task_context.py +268 -0
- aiecs/infrastructure/__init__.py +24 -0
- aiecs/infrastructure/graph_storage/__init__.py +11 -0
- aiecs/infrastructure/graph_storage/base.py +601 -0
- aiecs/infrastructure/graph_storage/batch_operations.py +449 -0
- aiecs/infrastructure/graph_storage/cache.py +429 -0
- aiecs/infrastructure/graph_storage/distributed.py +226 -0
- aiecs/infrastructure/graph_storage/error_handling.py +390 -0
- aiecs/infrastructure/graph_storage/graceful_degradation.py +306 -0
- aiecs/infrastructure/graph_storage/health_checks.py +378 -0
- aiecs/infrastructure/graph_storage/in_memory.py +514 -0
- aiecs/infrastructure/graph_storage/index_optimization.py +483 -0
- aiecs/infrastructure/graph_storage/lazy_loading.py +410 -0
- aiecs/infrastructure/graph_storage/metrics.py +357 -0
- aiecs/infrastructure/graph_storage/migration.py +413 -0
- aiecs/infrastructure/graph_storage/pagination.py +471 -0
- aiecs/infrastructure/graph_storage/performance_monitoring.py +466 -0
- aiecs/infrastructure/graph_storage/postgres.py +871 -0
- aiecs/infrastructure/graph_storage/query_optimizer.py +635 -0
- aiecs/infrastructure/graph_storage/schema_cache.py +290 -0
- aiecs/infrastructure/graph_storage/sqlite.py +623 -0
- aiecs/infrastructure/graph_storage/streaming.py +495 -0
- aiecs/infrastructure/messaging/__init__.py +13 -0
- aiecs/infrastructure/messaging/celery_task_manager.py +383 -0
- aiecs/infrastructure/messaging/websocket_manager.py +298 -0
- aiecs/infrastructure/monitoring/__init__.py +34 -0
- aiecs/infrastructure/monitoring/executor_metrics.py +174 -0
- aiecs/infrastructure/monitoring/global_metrics_manager.py +213 -0
- aiecs/infrastructure/monitoring/structured_logger.py +48 -0
- aiecs/infrastructure/monitoring/tracing_manager.py +410 -0
- aiecs/infrastructure/persistence/__init__.py +24 -0
- aiecs/infrastructure/persistence/context_engine_client.py +187 -0
- aiecs/infrastructure/persistence/database_manager.py +333 -0
- aiecs/infrastructure/persistence/file_storage.py +754 -0
- aiecs/infrastructure/persistence/redis_client.py +220 -0
- aiecs/llm/__init__.py +86 -0
- aiecs/llm/callbacks/__init__.py +11 -0
- aiecs/llm/callbacks/custom_callbacks.py +264 -0
- aiecs/llm/client_factory.py +420 -0
- aiecs/llm/clients/__init__.py +33 -0
- aiecs/llm/clients/base_client.py +193 -0
- aiecs/llm/clients/googleai_client.py +181 -0
- aiecs/llm/clients/openai_client.py +131 -0
- aiecs/llm/clients/vertex_client.py +437 -0
- aiecs/llm/clients/xai_client.py +184 -0
- aiecs/llm/config/__init__.py +51 -0
- aiecs/llm/config/config_loader.py +275 -0
- aiecs/llm/config/config_validator.py +236 -0
- aiecs/llm/config/model_config.py +151 -0
- aiecs/llm/utils/__init__.py +10 -0
- aiecs/llm/utils/validate_config.py +91 -0
- aiecs/main.py +363 -0
- aiecs/scripts/__init__.py +3 -0
- aiecs/scripts/aid/VERSION_MANAGEMENT.md +97 -0
- aiecs/scripts/aid/__init__.py +19 -0
- aiecs/scripts/aid/version_manager.py +215 -0
- aiecs/scripts/dependance_check/DEPENDENCY_SYSTEM_SUMMARY.md +242 -0
- aiecs/scripts/dependance_check/README_DEPENDENCY_CHECKER.md +310 -0
- aiecs/scripts/dependance_check/__init__.py +17 -0
- aiecs/scripts/dependance_check/dependency_checker.py +938 -0
- aiecs/scripts/dependance_check/dependency_fixer.py +391 -0
- aiecs/scripts/dependance_check/download_nlp_data.py +396 -0
- aiecs/scripts/dependance_check/quick_dependency_check.py +270 -0
- aiecs/scripts/dependance_check/setup_nlp_data.sh +217 -0
- aiecs/scripts/dependance_patch/__init__.py +7 -0
- aiecs/scripts/dependance_patch/fix_weasel/README_WEASEL_PATCH.md +126 -0
- aiecs/scripts/dependance_patch/fix_weasel/__init__.py +11 -0
- aiecs/scripts/dependance_patch/fix_weasel/fix_weasel_validator.py +128 -0
- aiecs/scripts/dependance_patch/fix_weasel/fix_weasel_validator.sh +82 -0
- aiecs/scripts/dependance_patch/fix_weasel/patch_weasel_library.sh +188 -0
- aiecs/scripts/dependance_patch/fix_weasel/run_weasel_patch.sh +41 -0
- aiecs/scripts/tools_develop/README.md +449 -0
- aiecs/scripts/tools_develop/TOOL_AUTO_DISCOVERY.md +234 -0
- aiecs/scripts/tools_develop/__init__.py +21 -0
- aiecs/scripts/tools_develop/check_type_annotations.py +259 -0
- aiecs/scripts/tools_develop/validate_tool_schemas.py +422 -0
- aiecs/scripts/tools_develop/verify_tools.py +356 -0
- aiecs/tasks/__init__.py +1 -0
- aiecs/tasks/worker.py +172 -0
- aiecs/tools/__init__.py +299 -0
- aiecs/tools/apisource/__init__.py +99 -0
- aiecs/tools/apisource/intelligence/__init__.py +19 -0
- aiecs/tools/apisource/intelligence/data_fusion.py +381 -0
- aiecs/tools/apisource/intelligence/query_analyzer.py +413 -0
- aiecs/tools/apisource/intelligence/search_enhancer.py +388 -0
- aiecs/tools/apisource/monitoring/__init__.py +9 -0
- aiecs/tools/apisource/monitoring/metrics.py +303 -0
- aiecs/tools/apisource/providers/__init__.py +115 -0
- aiecs/tools/apisource/providers/base.py +664 -0
- aiecs/tools/apisource/providers/census.py +401 -0
- aiecs/tools/apisource/providers/fred.py +564 -0
- aiecs/tools/apisource/providers/newsapi.py +412 -0
- aiecs/tools/apisource/providers/worldbank.py +357 -0
- aiecs/tools/apisource/reliability/__init__.py +12 -0
- aiecs/tools/apisource/reliability/error_handler.py +375 -0
- aiecs/tools/apisource/reliability/fallback_strategy.py +391 -0
- aiecs/tools/apisource/tool.py +850 -0
- aiecs/tools/apisource/utils/__init__.py +9 -0
- aiecs/tools/apisource/utils/validators.py +338 -0
- aiecs/tools/base_tool.py +201 -0
- aiecs/tools/docs/__init__.py +121 -0
- aiecs/tools/docs/ai_document_orchestrator.py +599 -0
- aiecs/tools/docs/ai_document_writer_orchestrator.py +2403 -0
- aiecs/tools/docs/content_insertion_tool.py +1333 -0
- aiecs/tools/docs/document_creator_tool.py +1317 -0
- aiecs/tools/docs/document_layout_tool.py +1166 -0
- aiecs/tools/docs/document_parser_tool.py +994 -0
- aiecs/tools/docs/document_writer_tool.py +1818 -0
- aiecs/tools/knowledge_graph/__init__.py +17 -0
- aiecs/tools/knowledge_graph/graph_reasoning_tool.py +734 -0
- aiecs/tools/knowledge_graph/graph_search_tool.py +923 -0
- aiecs/tools/knowledge_graph/kg_builder_tool.py +476 -0
- aiecs/tools/langchain_adapter.py +542 -0
- aiecs/tools/schema_generator.py +275 -0
- aiecs/tools/search_tool/__init__.py +100 -0
- aiecs/tools/search_tool/analyzers.py +589 -0
- aiecs/tools/search_tool/cache.py +260 -0
- aiecs/tools/search_tool/constants.py +128 -0
- aiecs/tools/search_tool/context.py +216 -0
- aiecs/tools/search_tool/core.py +749 -0
- aiecs/tools/search_tool/deduplicator.py +123 -0
- aiecs/tools/search_tool/error_handler.py +271 -0
- aiecs/tools/search_tool/metrics.py +371 -0
- aiecs/tools/search_tool/rate_limiter.py +178 -0
- aiecs/tools/search_tool/schemas.py +277 -0
- aiecs/tools/statistics/__init__.py +80 -0
- aiecs/tools/statistics/ai_data_analysis_orchestrator.py +643 -0
- aiecs/tools/statistics/ai_insight_generator_tool.py +505 -0
- aiecs/tools/statistics/ai_report_orchestrator_tool.py +694 -0
- aiecs/tools/statistics/data_loader_tool.py +564 -0
- aiecs/tools/statistics/data_profiler_tool.py +658 -0
- aiecs/tools/statistics/data_transformer_tool.py +573 -0
- aiecs/tools/statistics/data_visualizer_tool.py +495 -0
- aiecs/tools/statistics/model_trainer_tool.py +487 -0
- aiecs/tools/statistics/statistical_analyzer_tool.py +459 -0
- aiecs/tools/task_tools/__init__.py +86 -0
- aiecs/tools/task_tools/chart_tool.py +732 -0
- aiecs/tools/task_tools/classfire_tool.py +922 -0
- aiecs/tools/task_tools/image_tool.py +447 -0
- aiecs/tools/task_tools/office_tool.py +684 -0
- aiecs/tools/task_tools/pandas_tool.py +635 -0
- aiecs/tools/task_tools/report_tool.py +635 -0
- aiecs/tools/task_tools/research_tool.py +392 -0
- aiecs/tools/task_tools/scraper_tool.py +715 -0
- aiecs/tools/task_tools/stats_tool.py +688 -0
- aiecs/tools/temp_file_manager.py +130 -0
- aiecs/tools/tool_executor/__init__.py +37 -0
- aiecs/tools/tool_executor/tool_executor.py +881 -0
- aiecs/utils/LLM_output_structor.py +445 -0
- aiecs/utils/__init__.py +34 -0
- aiecs/utils/base_callback.py +47 -0
- aiecs/utils/cache_provider.py +695 -0
- aiecs/utils/execution_utils.py +184 -0
- aiecs/utils/logging.py +1 -0
- aiecs/utils/prompt_loader.py +14 -0
- aiecs/utils/token_usage_repository.py +323 -0
- aiecs/ws/__init__.py +0 -0
- aiecs/ws/socket_server.py +52 -0
- aiecs-1.5.1.dist-info/METADATA +608 -0
- aiecs-1.5.1.dist-info/RECORD +302 -0
- aiecs-1.5.1.dist-info/WHEEL +5 -0
- aiecs-1.5.1.dist-info/entry_points.txt +10 -0
- aiecs-1.5.1.dist-info/licenses/LICENSE +225 -0
- aiecs-1.5.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import redis.asyncio as redis
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Optional
|
|
4
|
+
import os
|
|
5
|
+
|
|
6
|
+
logger = logging.getLogger(__name__)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class RedisClient:
|
|
10
|
+
"""Redis client singleton for sharing across different caching strategies"""
|
|
11
|
+
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self._client: Optional[redis.Redis] = None
|
|
14
|
+
self._connection_pool: Optional[redis.ConnectionPool] = None
|
|
15
|
+
|
|
16
|
+
async def initialize(self):
|
|
17
|
+
"""Initialize Redis client"""
|
|
18
|
+
try:
|
|
19
|
+
# Get Redis configuration from environment variables
|
|
20
|
+
redis_host = os.getenv("REDIS_HOST", "localhost")
|
|
21
|
+
redis_port = int(os.getenv("REDIS_PORT", 6379))
|
|
22
|
+
redis_db = int(os.getenv("REDIS_DB", 0))
|
|
23
|
+
redis_password = os.getenv("REDIS_PASSWORD")
|
|
24
|
+
|
|
25
|
+
# Create connection pool
|
|
26
|
+
self._connection_pool = redis.ConnectionPool(
|
|
27
|
+
host=redis_host,
|
|
28
|
+
port=redis_port,
|
|
29
|
+
db=redis_db,
|
|
30
|
+
password=redis_password,
|
|
31
|
+
decode_responses=True,
|
|
32
|
+
max_connections=20,
|
|
33
|
+
retry_on_timeout=True,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Create Redis client
|
|
37
|
+
self._client = redis.Redis(connection_pool=self._connection_pool)
|
|
38
|
+
|
|
39
|
+
# Test connection
|
|
40
|
+
await self._client.ping()
|
|
41
|
+
logger.info(
|
|
42
|
+
f"Redis client initialized successfully: {redis_host}:{redis_port}/{redis_db}"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
except Exception as e:
|
|
46
|
+
logger.error(f"Failed to initialize Redis client: {e}")
|
|
47
|
+
raise
|
|
48
|
+
|
|
49
|
+
async def get_client(self) -> redis.Redis:
|
|
50
|
+
"""Get Redis client instance"""
|
|
51
|
+
if self._client is None:
|
|
52
|
+
raise RuntimeError("Redis client not initialized. Call initialize() first.")
|
|
53
|
+
return self._client
|
|
54
|
+
|
|
55
|
+
async def close(self):
|
|
56
|
+
"""Close Redis connection"""
|
|
57
|
+
if self._client:
|
|
58
|
+
await self._client.close()
|
|
59
|
+
self._client = None
|
|
60
|
+
if self._connection_pool:
|
|
61
|
+
await self._connection_pool.disconnect()
|
|
62
|
+
self._connection_pool = None
|
|
63
|
+
logger.info("Redis client closed")
|
|
64
|
+
|
|
65
|
+
async def hincrby(self, name: str, key: str, amount: int = 1) -> int:
|
|
66
|
+
"""Atomically increment hash field"""
|
|
67
|
+
client = await self.get_client()
|
|
68
|
+
return await client.hincrby(name, key, amount)
|
|
69
|
+
|
|
70
|
+
async def hget(self, name: str, key: str) -> Optional[str]:
|
|
71
|
+
"""Get hash field value"""
|
|
72
|
+
client = await self.get_client()
|
|
73
|
+
return await client.hget(name, key)
|
|
74
|
+
|
|
75
|
+
async def hgetall(self, name: str) -> dict:
|
|
76
|
+
"""Get all hash fields"""
|
|
77
|
+
client = await self.get_client()
|
|
78
|
+
return await client.hgetall(name)
|
|
79
|
+
|
|
80
|
+
async def hset(
|
|
81
|
+
self,
|
|
82
|
+
name: str,
|
|
83
|
+
key: Optional[str] = None,
|
|
84
|
+
value: Optional[str] = None,
|
|
85
|
+
mapping: Optional[dict] = None,
|
|
86
|
+
) -> int:
|
|
87
|
+
"""Set hash fields
|
|
88
|
+
|
|
89
|
+
Supports two calling patterns:
|
|
90
|
+
1. hset(name, key, value) - Set single field (positional)
|
|
91
|
+
2. hset(name, key=key, value=value) - Set single field (keyword)
|
|
92
|
+
3. hset(name, mapping={...}) - Set multiple fields
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
name: Redis hash key name
|
|
96
|
+
key: Field name (for single field set)
|
|
97
|
+
value: Field value (for single field set)
|
|
98
|
+
mapping: Dictionary of field-value pairs (for multiple fields)
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Number of fields that were added
|
|
102
|
+
|
|
103
|
+
Raises:
|
|
104
|
+
ValueError: If neither (key, value) nor mapping is provided
|
|
105
|
+
|
|
106
|
+
Examples:
|
|
107
|
+
# Single field with positional args
|
|
108
|
+
await redis_client.hset("myhash", "field1", "value1")
|
|
109
|
+
|
|
110
|
+
# Single field with keyword args
|
|
111
|
+
await redis_client.hset("myhash", key="field1", value="value1")
|
|
112
|
+
|
|
113
|
+
# Multiple fields with mapping
|
|
114
|
+
await redis_client.hset("myhash", mapping={"field1": "value1", "field2": "value2"})
|
|
115
|
+
"""
|
|
116
|
+
client = await self.get_client()
|
|
117
|
+
|
|
118
|
+
if mapping is not None:
|
|
119
|
+
# Multiple fields mode
|
|
120
|
+
return await client.hset(name, mapping=mapping)
|
|
121
|
+
elif key is not None and value is not None:
|
|
122
|
+
# Single field mode
|
|
123
|
+
return await client.hset(name, key=key, value=value)
|
|
124
|
+
else:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
"Either provide (key, value) or mapping parameter. "
|
|
127
|
+
f"Got: key={key}, value={value}, mapping={mapping}"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
async def expire(self, name: str, time: int) -> bool:
|
|
131
|
+
"""Set expiration time"""
|
|
132
|
+
client = await self.get_client()
|
|
133
|
+
return await client.expire(name, time)
|
|
134
|
+
|
|
135
|
+
async def exists(self, name: str) -> bool:
|
|
136
|
+
"""Check if key exists"""
|
|
137
|
+
client = await self.get_client()
|
|
138
|
+
return bool(await client.exists(name))
|
|
139
|
+
|
|
140
|
+
async def ping(self) -> bool:
|
|
141
|
+
"""Test Redis connection"""
|
|
142
|
+
try:
|
|
143
|
+
client = await self.get_client()
|
|
144
|
+
result = await client.ping()
|
|
145
|
+
return result
|
|
146
|
+
except Exception as e:
|
|
147
|
+
logger.error(f"Redis ping failed: {e}")
|
|
148
|
+
return False
|
|
149
|
+
|
|
150
|
+
async def info(self, section: str = None) -> dict:
|
|
151
|
+
"""Get Redis server information"""
|
|
152
|
+
try:
|
|
153
|
+
client = await self.get_client()
|
|
154
|
+
return await client.info(section)
|
|
155
|
+
except Exception as e:
|
|
156
|
+
logger.error(f"Redis info failed: {e}")
|
|
157
|
+
return {}
|
|
158
|
+
|
|
159
|
+
async def delete(self, *keys) -> int:
|
|
160
|
+
"""Delete one or more keys"""
|
|
161
|
+
try:
|
|
162
|
+
client = await self.get_client()
|
|
163
|
+
return await client.delete(*keys)
|
|
164
|
+
except Exception as e:
|
|
165
|
+
logger.error(f"Redis delete failed: {e}")
|
|
166
|
+
return 0
|
|
167
|
+
|
|
168
|
+
async def set(self, key: str, value: str, ex: int = None) -> bool:
|
|
169
|
+
"""Set a key-value pair with optional expiration"""
|
|
170
|
+
try:
|
|
171
|
+
client = await self.get_client()
|
|
172
|
+
return await client.set(key, value, ex=ex)
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.error(f"Redis set failed for key {key}: {e}")
|
|
175
|
+
return False
|
|
176
|
+
|
|
177
|
+
async def get(self, key: str) -> Optional[str]:
|
|
178
|
+
"""Get value by key"""
|
|
179
|
+
try:
|
|
180
|
+
client = await self.get_client()
|
|
181
|
+
return await client.get(key)
|
|
182
|
+
except Exception as e:
|
|
183
|
+
logger.error(f"Redis get failed for key {key}: {e}")
|
|
184
|
+
return None
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ✅ Key changes:
|
|
188
|
+
# 1. No longer create instance immediately.
|
|
189
|
+
# 2. Define a global variable with initial value None. This variable will
|
|
190
|
+
# be populated by lifespan.
|
|
191
|
+
redis_client: Optional[RedisClient] = None
|
|
192
|
+
|
|
193
|
+
# 3. Provide an initialization function for lifespan to call
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
async def initialize_redis_client():
|
|
197
|
+
"""Create and initialize global Redis client instance at application startup."""
|
|
198
|
+
global redis_client
|
|
199
|
+
if redis_client is None:
|
|
200
|
+
redis_client = RedisClient()
|
|
201
|
+
await redis_client.initialize()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# 4. Provide a close function for lifespan to call
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
async def close_redis_client():
|
|
208
|
+
"""Close global Redis client instance at application shutdown."""
|
|
209
|
+
if redis_client:
|
|
210
|
+
await redis_client.close()
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
# For backward compatibility, keep get_redis_client function
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
async def get_redis_client() -> RedisClient:
|
|
217
|
+
"""Get global Redis client instance"""
|
|
218
|
+
if redis_client is None:
|
|
219
|
+
raise RuntimeError("Redis client not initialized. Call initialize_redis_client() first.")
|
|
220
|
+
return redis_client
|
aiecs/llm/__init__.py
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""
|
|
2
|
+
LLM Package - Modular AI Provider Architecture
|
|
3
|
+
|
|
4
|
+
This package provides a unified interface to multiple AI providers through
|
|
5
|
+
individual client implementations and a factory pattern.
|
|
6
|
+
|
|
7
|
+
Package Structure:
|
|
8
|
+
- clients/: LLM client implementations
|
|
9
|
+
- config/: Configuration management
|
|
10
|
+
- callbacks/: Callback handlers
|
|
11
|
+
- utils/: Utility functions and scripts
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
# Import from organized subpackages
|
|
15
|
+
from .clients import (
|
|
16
|
+
BaseLLMClient,
|
|
17
|
+
LLMMessage,
|
|
18
|
+
LLMResponse,
|
|
19
|
+
LLMClientError,
|
|
20
|
+
ProviderNotAvailableError,
|
|
21
|
+
RateLimitError,
|
|
22
|
+
OpenAIClient,
|
|
23
|
+
VertexAIClient,
|
|
24
|
+
GoogleAIClient,
|
|
25
|
+
XAIClient,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from .client_factory import (
|
|
29
|
+
AIProvider,
|
|
30
|
+
LLMClientFactory,
|
|
31
|
+
LLMClientManager,
|
|
32
|
+
get_llm_manager,
|
|
33
|
+
generate_text,
|
|
34
|
+
stream_text,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
from .config import (
|
|
38
|
+
ModelCostConfig,
|
|
39
|
+
ModelCapabilities,
|
|
40
|
+
ModelDefaultParams,
|
|
41
|
+
ModelConfig,
|
|
42
|
+
ProviderConfig,
|
|
43
|
+
LLMModelsConfig,
|
|
44
|
+
LLMConfigLoader,
|
|
45
|
+
get_llm_config_loader,
|
|
46
|
+
get_llm_config,
|
|
47
|
+
reload_llm_config,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
from .callbacks import CustomAsyncCallbackHandler
|
|
51
|
+
|
|
52
|
+
__all__ = [
|
|
53
|
+
# Base classes and types
|
|
54
|
+
"BaseLLMClient",
|
|
55
|
+
"LLMMessage",
|
|
56
|
+
"LLMResponse",
|
|
57
|
+
"LLMClientError",
|
|
58
|
+
"ProviderNotAvailableError",
|
|
59
|
+
"RateLimitError",
|
|
60
|
+
"AIProvider",
|
|
61
|
+
# Factory and manager
|
|
62
|
+
"LLMClientFactory",
|
|
63
|
+
"LLMClientManager",
|
|
64
|
+
"get_llm_manager",
|
|
65
|
+
# Individual clients
|
|
66
|
+
"OpenAIClient",
|
|
67
|
+
"VertexAIClient",
|
|
68
|
+
"GoogleAIClient",
|
|
69
|
+
"XAIClient",
|
|
70
|
+
# Convenience functions
|
|
71
|
+
"generate_text",
|
|
72
|
+
"stream_text",
|
|
73
|
+
# Configuration management
|
|
74
|
+
"ModelCostConfig",
|
|
75
|
+
"ModelCapabilities",
|
|
76
|
+
"ModelDefaultParams",
|
|
77
|
+
"ModelConfig",
|
|
78
|
+
"ProviderConfig",
|
|
79
|
+
"LLMModelsConfig",
|
|
80
|
+
"LLMConfigLoader",
|
|
81
|
+
"get_llm_config_loader",
|
|
82
|
+
"get_llm_config",
|
|
83
|
+
"reload_llm_config",
|
|
84
|
+
# Callbacks
|
|
85
|
+
"CustomAsyncCallbackHandler",
|
|
86
|
+
]
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
from typing import Any, List, Optional
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
# Import the base callback handler from utils
|
|
5
|
+
from aiecs.utils.base_callback import CustomAsyncCallbackHandler
|
|
6
|
+
|
|
7
|
+
# Import LLM types for internal use only
|
|
8
|
+
# Import token usage repository
|
|
9
|
+
from aiecs.utils.token_usage_repository import token_usage_repo
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class RedisTokenCallbackHandler(CustomAsyncCallbackHandler):
|
|
15
|
+
"""
|
|
16
|
+
Concrete token recording callback handler.
|
|
17
|
+
Responsible for recording token usage after LLM calls by delegating to the repository.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, user_id: str, cycle_start_date: Optional[str] = None):
|
|
21
|
+
if not user_id:
|
|
22
|
+
raise ValueError("user_id must be provided for RedisTokenCallbackHandler")
|
|
23
|
+
self.user_id = user_id
|
|
24
|
+
self.cycle_start_date = cycle_start_date
|
|
25
|
+
self.start_time = None
|
|
26
|
+
self.messages = None
|
|
27
|
+
|
|
28
|
+
async def on_llm_start(self, messages: List[dict], **kwargs: Any) -> None:
|
|
29
|
+
"""Triggered when LLM call starts"""
|
|
30
|
+
import time
|
|
31
|
+
|
|
32
|
+
self.start_time = time.time()
|
|
33
|
+
self.messages = messages
|
|
34
|
+
|
|
35
|
+
logger.info(
|
|
36
|
+
f"[Callback] LLM call started for user '{self.user_id}' with {len(messages)} messages"
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
async def on_llm_end(self, response: dict, **kwargs: Any) -> None:
|
|
40
|
+
"""Triggered when LLM call ends successfully"""
|
|
41
|
+
try:
|
|
42
|
+
# Record call duration
|
|
43
|
+
if self.start_time:
|
|
44
|
+
import time
|
|
45
|
+
|
|
46
|
+
call_duration = time.time() - self.start_time
|
|
47
|
+
logger.info(
|
|
48
|
+
f"[Callback] LLM call completed for user '{self.user_id}' in {call_duration:.2f}s"
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
# Extract token usage from response dictionary
|
|
52
|
+
tokens_used = response.get("tokens_used")
|
|
53
|
+
|
|
54
|
+
if tokens_used and tokens_used > 0:
|
|
55
|
+
# Delegate recording work to repository
|
|
56
|
+
await token_usage_repo.increment_total_usage(
|
|
57
|
+
self.user_id, tokens_used, self.cycle_start_date
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
logger.info(f"[Callback] Recorded {tokens_used} tokens for user '{self.user_id}'")
|
|
61
|
+
else:
|
|
62
|
+
logger.warning(
|
|
63
|
+
f"[Callback] No token usage data available for user '{self.user_id}'"
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.error(f"[Callback] Failed to record token usage for user '{self.user_id}': {e}")
|
|
68
|
+
# Don't re-raise exception to avoid affecting main LLM call flow
|
|
69
|
+
|
|
70
|
+
async def on_llm_error(self, error: Exception, **kwargs: Any) -> None:
|
|
71
|
+
"""Triggered when LLM call encounters an error"""
|
|
72
|
+
if self.start_time:
|
|
73
|
+
import time
|
|
74
|
+
|
|
75
|
+
call_duration = time.time() - self.start_time
|
|
76
|
+
logger.error(
|
|
77
|
+
f"[Callback] LLM call failed for user '{self.user_id}' after {call_duration:.2f}s: {error}"
|
|
78
|
+
)
|
|
79
|
+
else:
|
|
80
|
+
logger.error(f"[Callback] LLM call failed for user '{self.user_id}': {error}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class DetailedRedisTokenCallbackHandler(CustomAsyncCallbackHandler):
|
|
84
|
+
"""
|
|
85
|
+
Detailed token recording callback handler.
|
|
86
|
+
Records separate prompt and completion token usage in addition to total usage.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
def __init__(self, user_id: str, cycle_start_date: Optional[str] = None):
|
|
90
|
+
if not user_id:
|
|
91
|
+
raise ValueError("user_id must be provided for DetailedRedisTokenCallbackHandler")
|
|
92
|
+
self.user_id = user_id
|
|
93
|
+
self.cycle_start_date = cycle_start_date
|
|
94
|
+
self.start_time = None
|
|
95
|
+
self.messages = None
|
|
96
|
+
self.prompt_tokens = 0
|
|
97
|
+
|
|
98
|
+
async def on_llm_start(self, messages: List[dict], **kwargs: Any) -> None:
|
|
99
|
+
"""Triggered when LLM call starts"""
|
|
100
|
+
import time
|
|
101
|
+
|
|
102
|
+
self.start_time = time.time()
|
|
103
|
+
self.messages = messages
|
|
104
|
+
|
|
105
|
+
# Estimate input token count
|
|
106
|
+
self.prompt_tokens = self._estimate_prompt_tokens(messages)
|
|
107
|
+
|
|
108
|
+
logger.info(
|
|
109
|
+
f"[DetailedCallback] LLM call started for user '{self.user_id}' with estimated {self.prompt_tokens} prompt tokens"
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
async def on_llm_end(self, response: dict, **kwargs: Any) -> None:
|
|
113
|
+
"""Triggered when LLM call ends successfully"""
|
|
114
|
+
try:
|
|
115
|
+
# Record call duration
|
|
116
|
+
if self.start_time:
|
|
117
|
+
import time
|
|
118
|
+
|
|
119
|
+
call_duration = time.time() - self.start_time
|
|
120
|
+
logger.info(
|
|
121
|
+
f"[DetailedCallback] LLM call completed for user '{self.user_id}' in {call_duration:.2f}s"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Extract detailed token information from response
|
|
125
|
+
prompt_tokens, completion_tokens = self._extract_detailed_tokens(response)
|
|
126
|
+
|
|
127
|
+
# Ensure we have valid integers (not None)
|
|
128
|
+
prompt_tokens = prompt_tokens or 0
|
|
129
|
+
completion_tokens = completion_tokens or 0
|
|
130
|
+
|
|
131
|
+
if prompt_tokens > 0 or completion_tokens > 0:
|
|
132
|
+
# Use detailed token recording method
|
|
133
|
+
await token_usage_repo.increment_detailed_usage(
|
|
134
|
+
self.user_id,
|
|
135
|
+
prompt_tokens,
|
|
136
|
+
completion_tokens,
|
|
137
|
+
self.cycle_start_date,
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
logger.info(
|
|
141
|
+
f"[DetailedCallback] Recorded detailed tokens for user '{self.user_id}': prompt={prompt_tokens}, completion={completion_tokens}"
|
|
142
|
+
)
|
|
143
|
+
else:
|
|
144
|
+
logger.warning(
|
|
145
|
+
f"[DetailedCallback] No detailed token usage data available for user '{self.user_id}'"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
except Exception as e:
|
|
149
|
+
logger.error(
|
|
150
|
+
f"[DetailedCallback] Failed to record detailed token usage for user '{self.user_id}': {e}"
|
|
151
|
+
)
|
|
152
|
+
# Don't re-raise exception to avoid affecting main LLM call flow
|
|
153
|
+
|
|
154
|
+
async def on_llm_error(self, error: Exception, **kwargs: Any) -> None:
|
|
155
|
+
"""Triggered when LLM call encounters an error"""
|
|
156
|
+
if self.start_time:
|
|
157
|
+
import time
|
|
158
|
+
|
|
159
|
+
call_duration = time.time() - self.start_time
|
|
160
|
+
logger.error(
|
|
161
|
+
f"[DetailedCallback] LLM call failed for user '{self.user_id}' after {call_duration:.2f}s: {error}"
|
|
162
|
+
)
|
|
163
|
+
else:
|
|
164
|
+
logger.error(f"[DetailedCallback] LLM call failed for user '{self.user_id}': {error}")
|
|
165
|
+
|
|
166
|
+
def _estimate_prompt_tokens(self, messages: List[dict]) -> int:
|
|
167
|
+
"""Estimate token count for input messages"""
|
|
168
|
+
total_chars = sum(len(msg.get("content", "")) for msg in messages)
|
|
169
|
+
# Rough estimation: 4 characters ≈ 1 token
|
|
170
|
+
return total_chars // 4
|
|
171
|
+
|
|
172
|
+
def _extract_detailed_tokens(self, response: dict) -> tuple[int, int]:
|
|
173
|
+
"""
|
|
174
|
+
Extract detailed token information from response dictionary
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
tuple: (prompt_tokens, completion_tokens)
|
|
178
|
+
"""
|
|
179
|
+
# If response has detailed token information, use it first
|
|
180
|
+
prompt_tokens = response.get("prompt_tokens") or 0
|
|
181
|
+
completion_tokens = response.get("completion_tokens") or 0
|
|
182
|
+
|
|
183
|
+
if prompt_tokens > 0 and completion_tokens > 0:
|
|
184
|
+
return prompt_tokens, completion_tokens
|
|
185
|
+
|
|
186
|
+
# If only total token count is available, try to allocate
|
|
187
|
+
tokens_used = response.get("tokens_used") or 0
|
|
188
|
+
if tokens_used > 0:
|
|
189
|
+
# Use previously estimated prompt tokens
|
|
190
|
+
prompt_tokens = self.prompt_tokens
|
|
191
|
+
completion_tokens = max(0, tokens_used - prompt_tokens)
|
|
192
|
+
return prompt_tokens, completion_tokens
|
|
193
|
+
|
|
194
|
+
# If no token information, try to estimate from response content
|
|
195
|
+
content = response.get("content", "")
|
|
196
|
+
if content:
|
|
197
|
+
completion_tokens = len(content) // 4
|
|
198
|
+
prompt_tokens = self.prompt_tokens
|
|
199
|
+
return prompt_tokens, completion_tokens
|
|
200
|
+
|
|
201
|
+
return 0, 0
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
class CompositeCallbackHandler(CustomAsyncCallbackHandler):
|
|
205
|
+
"""
|
|
206
|
+
Composite callback handler that can execute multiple callback handlers simultaneously
|
|
207
|
+
"""
|
|
208
|
+
|
|
209
|
+
def __init__(self, handlers: List[CustomAsyncCallbackHandler]):
|
|
210
|
+
self.handlers = handlers or []
|
|
211
|
+
|
|
212
|
+
def add_handler(self, handler: CustomAsyncCallbackHandler):
|
|
213
|
+
"""Add a callback handler"""
|
|
214
|
+
self.handlers.append(handler)
|
|
215
|
+
|
|
216
|
+
async def on_llm_start(self, messages: List[dict], **kwargs: Any) -> None:
|
|
217
|
+
"""Execute start callbacks for all handlers"""
|
|
218
|
+
for handler in self.handlers:
|
|
219
|
+
try:
|
|
220
|
+
await handler.on_llm_start(messages, **kwargs)
|
|
221
|
+
except Exception as e:
|
|
222
|
+
logger.error(
|
|
223
|
+
f"Error in callback handler {type(handler).__name__}.on_llm_start: {e}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
async def on_llm_end(self, response: dict, **kwargs: Any) -> None:
|
|
227
|
+
"""Execute end callbacks for all handlers"""
|
|
228
|
+
for handler in self.handlers:
|
|
229
|
+
try:
|
|
230
|
+
await handler.on_llm_end(response, **kwargs)
|
|
231
|
+
except Exception as e:
|
|
232
|
+
logger.error(f"Error in callback handler {type(handler).__name__}.on_llm_end: {e}")
|
|
233
|
+
|
|
234
|
+
async def on_llm_error(self, error: Exception, **kwargs: Any) -> None:
|
|
235
|
+
"""Execute error callbacks for all handlers"""
|
|
236
|
+
for handler in self.handlers:
|
|
237
|
+
try:
|
|
238
|
+
await handler.on_llm_error(error, **kwargs)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.error(
|
|
241
|
+
f"Error in callback handler {type(handler).__name__}.on_llm_error: {e}"
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
# Convenience functions for creating common callback handlers
|
|
246
|
+
def create_token_callback(
|
|
247
|
+
user_id: str, cycle_start_date: Optional[str] = None
|
|
248
|
+
) -> RedisTokenCallbackHandler:
|
|
249
|
+
"""Create a basic token recording callback handler"""
|
|
250
|
+
return RedisTokenCallbackHandler(user_id, cycle_start_date)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def create_detailed_token_callback(
|
|
254
|
+
user_id: str, cycle_start_date: Optional[str] = None
|
|
255
|
+
) -> DetailedRedisTokenCallbackHandler:
|
|
256
|
+
"""Create a detailed token recording callback handler"""
|
|
257
|
+
return DetailedRedisTokenCallbackHandler(user_id, cycle_start_date)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def create_composite_callback(
|
|
261
|
+
*handlers: CustomAsyncCallbackHandler,
|
|
262
|
+
) -> CompositeCallbackHandler:
|
|
263
|
+
"""Create a composite callback handler"""
|
|
264
|
+
return CompositeCallbackHandler(list(handlers))
|