aiecs 1.7.6__py3-none-any.whl → 1.8.4__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 +1 -1
- aiecs/application/knowledge_graph/extractors/llm_entity_extractor.py +5 -1
- aiecs/application/knowledge_graph/retrieval/query_intent_classifier.py +7 -5
- aiecs/config/config.py +3 -0
- aiecs/config/tool_config.py +55 -19
- aiecs/domain/agent/base_agent.py +79 -0
- aiecs/domain/agent/hybrid_agent.py +552 -175
- aiecs/domain/agent/knowledge_aware_agent.py +3 -2
- aiecs/domain/agent/llm_agent.py +2 -0
- aiecs/domain/agent/models.py +10 -0
- aiecs/domain/agent/tools/schema_generator.py +17 -4
- aiecs/llm/callbacks/custom_callbacks.py +9 -4
- aiecs/llm/client_factory.py +20 -7
- aiecs/llm/clients/base_client.py +50 -5
- aiecs/llm/clients/google_function_calling_mixin.py +46 -88
- aiecs/llm/clients/googleai_client.py +183 -9
- aiecs/llm/clients/openai_client.py +12 -0
- aiecs/llm/clients/openai_compatible_mixin.py +42 -2
- aiecs/llm/clients/openrouter_client.py +272 -0
- aiecs/llm/clients/vertex_client.py +385 -22
- aiecs/llm/clients/xai_client.py +41 -3
- aiecs/llm/protocols.py +19 -1
- aiecs/llm/utils/image_utils.py +179 -0
- aiecs/main.py +2 -2
- aiecs/tools/docs/document_creator_tool.py +143 -2
- aiecs/tools/docs/document_parser_tool.py +9 -4
- aiecs/tools/docs/document_writer_tool.py +179 -0
- aiecs/tools/task_tools/image_tool.py +49 -14
- aiecs/tools/task_tools/scraper_tool.py +39 -2
- {aiecs-1.7.6.dist-info → aiecs-1.8.4.dist-info}/METADATA +4 -2
- {aiecs-1.7.6.dist-info → aiecs-1.8.4.dist-info}/RECORD +35 -33
- {aiecs-1.7.6.dist-info → aiecs-1.8.4.dist-info}/WHEEL +0 -0
- {aiecs-1.7.6.dist-info → aiecs-1.8.4.dist-info}/entry_points.txt +0 -0
- {aiecs-1.7.6.dist-info → aiecs-1.8.4.dist-info}/licenses/LICENSE +0 -0
- {aiecs-1.7.6.dist-info → aiecs-1.8.4.dist-info}/top_level.txt +0 -0
aiecs/__init__.py
CHANGED
|
@@ -5,7 +5,7 @@ A powerful Python middleware framework for building AI-powered applications
|
|
|
5
5
|
with tool orchestration, task execution, and multi-provider LLM support.
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
|
-
__version__ = "1.
|
|
8
|
+
__version__ = "1.8.4"
|
|
9
9
|
__author__ = "AIECS Team"
|
|
10
10
|
__email__ = "iretbl@gmail.com"
|
|
11
11
|
|
|
@@ -162,7 +162,7 @@ class LLMEntityExtractor(EntityExtractor):
|
|
|
162
162
|
Args:
|
|
163
163
|
text: Input text to extract entities from
|
|
164
164
|
entity_types: Optional filter for specific entity types
|
|
165
|
-
**kwargs: Additional parameters (e.g., custom prompt, examples)
|
|
165
|
+
**kwargs: Additional parameters (e.g., custom prompt, examples, context)
|
|
166
166
|
|
|
167
167
|
Returns:
|
|
168
168
|
List of extracted Entity objects
|
|
@@ -174,6 +174,9 @@ class LLMEntityExtractor(EntityExtractor):
|
|
|
174
174
|
if not text or not text.strip():
|
|
175
175
|
raise ValueError("Input text cannot be empty")
|
|
176
176
|
|
|
177
|
+
# Extract context from kwargs if provided
|
|
178
|
+
context = kwargs.get("context")
|
|
179
|
+
|
|
177
180
|
# Build extraction prompt
|
|
178
181
|
prompt = self._build_extraction_prompt(text, entity_types)
|
|
179
182
|
|
|
@@ -189,6 +192,7 @@ class LLMEntityExtractor(EntityExtractor):
|
|
|
189
192
|
model=self.model,
|
|
190
193
|
temperature=self.temperature,
|
|
191
194
|
max_tokens=self.max_tokens,
|
|
195
|
+
context=context,
|
|
192
196
|
)
|
|
193
197
|
# Otherwise use LLM manager with provider
|
|
194
198
|
else:
|
|
@@ -6,7 +6,7 @@ Uses a lightweight LLM to determine the best retrieval approach based on query c
|
|
|
6
6
|
"""
|
|
7
7
|
|
|
8
8
|
import logging
|
|
9
|
-
from typing import Optional, Dict, TYPE_CHECKING
|
|
9
|
+
from typing import Optional, Dict, Any, TYPE_CHECKING
|
|
10
10
|
from aiecs.application.knowledge_graph.retrieval.strategy_types import RetrievalStrategy
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
@@ -58,12 +58,13 @@ class QueryIntentClassifier:
|
|
|
58
58
|
self.enable_caching = enable_caching
|
|
59
59
|
self._cache: Dict[str, RetrievalStrategy] = {}
|
|
60
60
|
|
|
61
|
-
async def classify_intent(self, query: str) -> RetrievalStrategy:
|
|
61
|
+
async def classify_intent(self, query: str, context: Optional[Dict[str, Any]] = None) -> RetrievalStrategy:
|
|
62
62
|
"""
|
|
63
63
|
Classify query intent and return optimal retrieval strategy.
|
|
64
64
|
|
|
65
65
|
Args:
|
|
66
66
|
query: Query string to classify
|
|
67
|
+
context: Optional context dictionary for tracking/observability
|
|
67
68
|
|
|
68
69
|
Returns:
|
|
69
70
|
RetrievalStrategy enum value
|
|
@@ -82,7 +83,7 @@ class QueryIntentClassifier:
|
|
|
82
83
|
# Use LLM classification if client is available
|
|
83
84
|
if self.llm_client is not None:
|
|
84
85
|
try:
|
|
85
|
-
strategy = await self._classify_with_llm(query)
|
|
86
|
+
strategy = await self._classify_with_llm(query, context)
|
|
86
87
|
except Exception as e:
|
|
87
88
|
logger.warning(f"LLM classification failed: {e}, falling back to rule-based")
|
|
88
89
|
strategy = self._classify_with_rules(query)
|
|
@@ -96,7 +97,7 @@ class QueryIntentClassifier:
|
|
|
96
97
|
|
|
97
98
|
return strategy
|
|
98
99
|
|
|
99
|
-
async def _classify_with_llm(self, query: str) -> RetrievalStrategy:
|
|
100
|
+
async def _classify_with_llm(self, query: str, context: Optional[Dict[str, Any]] = None) -> RetrievalStrategy:
|
|
100
101
|
"""
|
|
101
102
|
Classify query using LLM.
|
|
102
103
|
|
|
@@ -127,11 +128,12 @@ Respond with ONLY the strategy name (e.g., "MULTI_HOP"). No explanation needed."
|
|
|
127
128
|
if self.llm_client is None:
|
|
128
129
|
# Fallback to rule-based classification if no LLM client
|
|
129
130
|
return self._classify_with_rules(query)
|
|
130
|
-
|
|
131
|
+
|
|
131
132
|
response = await self.llm_client.generate_text(
|
|
132
133
|
messages=messages,
|
|
133
134
|
temperature=0.0, # Deterministic classification
|
|
134
135
|
max_tokens=20, # Short response
|
|
136
|
+
context=context,
|
|
135
137
|
)
|
|
136
138
|
|
|
137
139
|
# Parse response
|
aiecs/config/config.py
CHANGED
|
@@ -47,6 +47,9 @@ class Settings(BaseSettings):
|
|
|
47
47
|
google_cse_id: str = Field(default="", alias="GOOGLE_CSE_ID")
|
|
48
48
|
xai_api_key: str = Field(default="", alias="XAI_API_KEY")
|
|
49
49
|
grok_api_key: str = Field(default="", alias="GROK_API_KEY") # Backward compatibility
|
|
50
|
+
openrouter_api_key: str = Field(default="", alias="OPENROUTER_API_KEY")
|
|
51
|
+
openrouter_http_referer: str = Field(default="", alias="OPENROUTER_HTTP_REFERER")
|
|
52
|
+
openrouter_x_title: str = Field(default="", alias="OPENROUTER_X_TITLE")
|
|
50
53
|
|
|
51
54
|
# LLM Models Configuration
|
|
52
55
|
llm_models_config_path: str = Field(
|
aiecs/config/tool_config.py
CHANGED
|
@@ -212,21 +212,51 @@ class ToolConfigLoader:
|
|
|
212
212
|
logger.warning(f"Failed to load {global_config_path}: {e}. Skipping.")
|
|
213
213
|
|
|
214
214
|
# Load tool-specific config (higher precedence)
|
|
215
|
+
# Try multiple locations:
|
|
216
|
+
# 1. config/tools/{tool_name}.yaml (standard location)
|
|
217
|
+
# 2. config/{tool_name}.yaml (direct in config_dir, for custom paths)
|
|
215
218
|
tools_dir = config_dir / "tools"
|
|
219
|
+
search_dirs = []
|
|
216
220
|
if tools_dir.exists() and tools_dir.is_dir():
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
221
|
+
search_dirs.append(tools_dir)
|
|
222
|
+
# Also search directly in config_dir for custom path structures
|
|
223
|
+
search_dirs.append(config_dir)
|
|
224
|
+
|
|
225
|
+
# Try multiple naming conventions for tool config files
|
|
226
|
+
# 1. {tool_name}.yaml (e.g., image.yaml)
|
|
227
|
+
# 2. {tool_name}_tool.yaml (e.g., image_tool.yaml)
|
|
228
|
+
# 3. {ToolName}.yaml (e.g., ImageTool.yaml)
|
|
229
|
+
possible_names = [
|
|
230
|
+
f"{tool_name}.yaml",
|
|
231
|
+
f"{tool_name}_tool.yaml",
|
|
232
|
+
]
|
|
233
|
+
# Also try with capitalized class name if tool_name is lowercase
|
|
234
|
+
if tool_name.islower():
|
|
235
|
+
class_name = tool_name.replace("_", "").title() + "Tool"
|
|
236
|
+
possible_names.append(f"{class_name}.yaml")
|
|
237
|
+
|
|
238
|
+
tool_config_path = None
|
|
239
|
+
for search_dir in search_dirs:
|
|
240
|
+
for name in possible_names:
|
|
241
|
+
candidate_path = search_dir / name
|
|
242
|
+
if candidate_path.exists():
|
|
243
|
+
tool_config_path = candidate_path
|
|
244
|
+
break
|
|
245
|
+
if tool_config_path:
|
|
246
|
+
break
|
|
247
|
+
|
|
248
|
+
if tool_config_path:
|
|
249
|
+
try:
|
|
250
|
+
with open(tool_config_path, "r", encoding="utf-8") as f:
|
|
251
|
+
tool_data = yaml.safe_load(f)
|
|
252
|
+
if tool_data:
|
|
253
|
+
# Merge tool-specific config (overrides global)
|
|
254
|
+
merged_config.update(tool_data)
|
|
255
|
+
logger.debug(f"Loaded tool-specific config from {tool_config_path}")
|
|
256
|
+
except yaml.YAMLError as e:
|
|
257
|
+
logger.warning(f"Invalid YAML in {tool_config_path}: {e}. Skipping.")
|
|
258
|
+
except Exception as e:
|
|
259
|
+
logger.warning(f"Failed to load {tool_config_path}: {e}. Skipping.")
|
|
230
260
|
|
|
231
261
|
return merged_config
|
|
232
262
|
|
|
@@ -362,17 +392,23 @@ class ToolConfigLoader:
|
|
|
362
392
|
logger.debug(f"Loaded config for {tool_name}: {len(merged_config)} keys")
|
|
363
393
|
return merged_config
|
|
364
394
|
|
|
365
|
-
def set_config_path(self, path: Union[str, Path]) -> None:
|
|
395
|
+
def set_config_path(self, path: Optional[Union[str, Path]] = None) -> None:
|
|
366
396
|
"""
|
|
367
397
|
Set custom config path.
|
|
368
398
|
|
|
369
399
|
Args:
|
|
370
|
-
path: Path to config directory or file
|
|
400
|
+
path: Path to config directory or file. If None, resets to auto-discovery.
|
|
371
401
|
"""
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
402
|
+
if path is None:
|
|
403
|
+
self._config_path = None
|
|
404
|
+
# Clear cached config directory to force re-discovery
|
|
405
|
+
self._cached_config_dir = None
|
|
406
|
+
logger.info("Reset config path to auto-discovery")
|
|
407
|
+
else:
|
|
408
|
+
self._config_path = Path(path)
|
|
409
|
+
# Clear cached config directory to force re-discovery
|
|
410
|
+
self._cached_config_dir = None
|
|
411
|
+
logger.info(f"Set custom config path: {self._config_path}")
|
|
376
412
|
|
|
377
413
|
def get_config_path(self) -> Optional[Path]:
|
|
378
414
|
"""
|
aiecs/domain/agent/base_agent.py
CHANGED
|
@@ -1296,6 +1296,67 @@ class BaseAIAgent(ABC):
|
|
|
1296
1296
|
|
|
1297
1297
|
self._metrics.updated_at = datetime.utcnow()
|
|
1298
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
|
+
|
|
1299
1360
|
def update_session_metrics(
|
|
1300
1361
|
self,
|
|
1301
1362
|
session_status: str,
|
|
@@ -1518,6 +1579,18 @@ class BaseAIAgent(ABC):
|
|
|
1518
1579
|
"p95_operation_time": self._metrics.p95_operation_time,
|
|
1519
1580
|
"p99_operation_time": self._metrics.p99_operation_time,
|
|
1520
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
|
+
},
|
|
1521
1594
|
}
|
|
1522
1595
|
|
|
1523
1596
|
def get_health_status(self) -> Dict[str, Any]:
|
|
@@ -1658,6 +1731,12 @@ class BaseAIAgent(ABC):
|
|
|
1658
1731
|
# Error tracking
|
|
1659
1732
|
"error_count": self._metrics.error_count,
|
|
1660
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,
|
|
1661
1740
|
},
|
|
1662
1741
|
"capabilities": [cap.capability_type for cap in self.get_capabilities()],
|
|
1663
1742
|
"active_goals": len([g for g in self._goals.values() if g.status == GoalStatus.IN_PROGRESS]),
|