rakam-systems-core 0.1.1rc7__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.
Files changed (34) hide show
  1. rakam_systems_core/__init__.py +41 -0
  2. rakam_systems_core/ai_core/__init__.py +68 -0
  3. rakam_systems_core/ai_core/base.py +142 -0
  4. rakam_systems_core/ai_core/config.py +12 -0
  5. rakam_systems_core/ai_core/config_loader.py +580 -0
  6. rakam_systems_core/ai_core/config_schema.py +395 -0
  7. rakam_systems_core/ai_core/interfaces/__init__.py +30 -0
  8. rakam_systems_core/ai_core/interfaces/agent.py +83 -0
  9. rakam_systems_core/ai_core/interfaces/chat_history.py +122 -0
  10. rakam_systems_core/ai_core/interfaces/chunker.py +11 -0
  11. rakam_systems_core/ai_core/interfaces/embedding_model.py +10 -0
  12. rakam_systems_core/ai_core/interfaces/indexer.py +10 -0
  13. rakam_systems_core/ai_core/interfaces/llm_gateway.py +139 -0
  14. rakam_systems_core/ai_core/interfaces/loader.py +86 -0
  15. rakam_systems_core/ai_core/interfaces/reranker.py +10 -0
  16. rakam_systems_core/ai_core/interfaces/retriever.py +11 -0
  17. rakam_systems_core/ai_core/interfaces/tool.py +162 -0
  18. rakam_systems_core/ai_core/interfaces/tool_invoker.py +260 -0
  19. rakam_systems_core/ai_core/interfaces/tool_loader.py +374 -0
  20. rakam_systems_core/ai_core/interfaces/tool_registry.py +287 -0
  21. rakam_systems_core/ai_core/interfaces/vectorstore.py +37 -0
  22. rakam_systems_core/ai_core/mcp/README.md +545 -0
  23. rakam_systems_core/ai_core/mcp/__init__.py +0 -0
  24. rakam_systems_core/ai_core/mcp/mcp_server.py +334 -0
  25. rakam_systems_core/ai_core/tracking.py +602 -0
  26. rakam_systems_core/ai_core/vs_core.py +55 -0
  27. rakam_systems_core/ai_utils/__init__.py +16 -0
  28. rakam_systems_core/ai_utils/logging.py +126 -0
  29. rakam_systems_core/ai_utils/metrics.py +10 -0
  30. rakam_systems_core/ai_utils/s3.py +480 -0
  31. rakam_systems_core/ai_utils/tracing.py +5 -0
  32. rakam_systems_core-0.1.1rc7.dist-info/METADATA +162 -0
  33. rakam_systems_core-0.1.1rc7.dist-info/RECORD +34 -0
  34. rakam_systems_core-0.1.1rc7.dist-info/WHEEL +4 -0
@@ -0,0 +1,580 @@
1
+ """
2
+ Configuration loader for agents.
3
+
4
+ This module provides functionality to load agent configurations from YAML files
5
+ and instantiate agents with the specified settings.
6
+ """
7
+ from __future__ import annotations
8
+ from typing import Any, Dict, List, Optional, Type, Callable
9
+ from pathlib import Path
10
+ import yaml
11
+ import importlib
12
+ import inspect
13
+
14
+ from pydantic import BaseModel, Field, create_model
15
+
16
+ from .config_schema import (
17
+ ConfigFileSchema,
18
+ AgentConfigSchema,
19
+ ToolConfigSchema,
20
+ ModelConfigSchema,
21
+ PromptConfigSchema,
22
+ OutputTypeSchema,
23
+ OutputFieldSchema,
24
+ ToolMode,
25
+ )
26
+ from .interfaces.tool_registry import ToolRegistry, ToolMetadata
27
+ from .interfaces.tool import ToolComponent
28
+ from .interfaces.agent import ModelSettings
29
+
30
+
31
+ class ConfigurationLoader:
32
+ """
33
+ Loads agent configurations from YAML files and creates agent instances.
34
+
35
+ Features:
36
+ - Load complete configuration from YAML
37
+ - Resolve references (tools, prompts)
38
+ - Dynamic module/class loading
39
+ - Tool registry integration
40
+ - Validation using Pydantic schemas
41
+
42
+ Example:
43
+ >>> loader = ConfigurationLoader()
44
+ >>> config = loader.load_from_yaml("agent_config.yaml")
45
+ >>> agent = loader.create_agent("my_agent", config)
46
+ """
47
+
48
+ def __init__(self):
49
+ self.config: Optional[ConfigFileSchema] = None
50
+ self.tool_registry: Optional[ToolRegistry] = None
51
+
52
+ def load_from_yaml(self, config_path: str) -> ConfigFileSchema:
53
+ """
54
+ Load configuration from YAML file.
55
+
56
+ Args:
57
+ config_path: Path to YAML configuration file
58
+
59
+ Returns:
60
+ Validated configuration schema
61
+ """
62
+ path = Path(config_path)
63
+ if not path.exists():
64
+ raise FileNotFoundError(f"Configuration file not found: {config_path}")
65
+
66
+ with open(path, 'r') as f:
67
+ raw_config = yaml.safe_load(f)
68
+
69
+ # Validate using Pydantic
70
+ self.config = ConfigFileSchema(**raw_config)
71
+ return self.config
72
+
73
+ def load_from_dict(self, config_dict: Dict[str, Any]) -> ConfigFileSchema:
74
+ """
75
+ Load configuration from dictionary.
76
+
77
+ Args:
78
+ config_dict: Configuration dictionary
79
+
80
+ Returns:
81
+ Validated configuration schema
82
+ """
83
+ self.config = ConfigFileSchema(**config_dict)
84
+ return self.config
85
+
86
+ def get_tool_registry(self, config: Optional[ConfigFileSchema] = None) -> ToolRegistry:
87
+ """
88
+ Get or create a tool registry from configuration.
89
+
90
+ Args:
91
+ config: Configuration to use (uses loaded config if None)
92
+
93
+ Returns:
94
+ ToolRegistry with all configured tools
95
+ """
96
+ if self.tool_registry is not None:
97
+ return self.tool_registry
98
+
99
+ config = config or self.config
100
+ if config is None:
101
+ raise ValueError("No configuration loaded. Call load_from_yaml or load_from_dict first.")
102
+
103
+ registry = ToolRegistry()
104
+
105
+ # Register all tools from config
106
+ for tool_name, tool_config in config.tools.items():
107
+ self._register_tool(registry, tool_config)
108
+
109
+ self.tool_registry = registry
110
+ return registry
111
+
112
+ def _register_tool(self, registry: ToolRegistry, tool_config: ToolConfigSchema) -> None:
113
+ """
114
+ Register a single tool in the registry.
115
+
116
+ Args:
117
+ registry: ToolRegistry to register in
118
+ tool_config: Tool configuration
119
+ """
120
+ if tool_config.type == ToolMode.DIRECT:
121
+ # Load the function dynamically
122
+ function = self._load_function(tool_config.module, tool_config.function)
123
+
124
+ # Register with registry
125
+ registry.register_direct_tool(
126
+ name=tool_config.name,
127
+ function=function,
128
+ description=tool_config.description,
129
+ json_schema=tool_config.json_schema or self._generate_schema(function),
130
+ takes_ctx=tool_config.takes_ctx,
131
+ category=tool_config.category,
132
+ tags=tool_config.tags,
133
+ )
134
+
135
+ elif tool_config.type == ToolMode.MCP:
136
+ # Register MCP tool
137
+ registry.register_mcp_tool(
138
+ name=tool_config.name,
139
+ mcp_server=tool_config.mcp_server,
140
+ mcp_tool_name=tool_config.mcp_tool_name or tool_config.name,
141
+ description=tool_config.description,
142
+ category=tool_config.category,
143
+ tags=tool_config.tags,
144
+ )
145
+
146
+ def _load_function(self, module_path: str, function_name: str) -> Callable:
147
+ """
148
+ Dynamically load a function from a module.
149
+
150
+ Args:
151
+ module_path: Python module path (e.g., 'myapp.tools')
152
+ function_name: Function name to load
153
+
154
+ Returns:
155
+ The loaded function
156
+ """
157
+ try:
158
+ module = importlib.import_module(module_path)
159
+ function = getattr(module, function_name)
160
+
161
+ if not callable(function):
162
+ raise ValueError(f"{function_name} in {module_path} is not callable")
163
+
164
+ return function
165
+ except ImportError as e:
166
+ raise ImportError(f"Failed to import module {module_path}: {e}")
167
+ except AttributeError as e:
168
+ raise AttributeError(f"Function {function_name} not found in {module_path}: {e}")
169
+
170
+ def _load_class(self, class_path: str) -> Type:
171
+ """
172
+ Dynamically load a class from a module.
173
+
174
+ Args:
175
+ class_path: Fully qualified class path (e.g., 'myapp.models.MyClass')
176
+
177
+ Returns:
178
+ The loaded class
179
+ """
180
+ parts = class_path.rsplit('.', 1)
181
+ if len(parts) != 2:
182
+ raise ValueError(f"Invalid class path: {class_path}")
183
+
184
+ module_path, class_name = parts
185
+ try:
186
+ module = importlib.import_module(module_path)
187
+ cls = getattr(module, class_name)
188
+
189
+ if not isinstance(cls, type):
190
+ raise ValueError(f"{class_name} in {module_path} is not a class")
191
+
192
+ return cls
193
+ except ImportError as e:
194
+ raise ImportError(f"Failed to import module {module_path}: {e}")
195
+ except AttributeError as e:
196
+ raise AttributeError(f"Class {class_name} not found in {module_path}: {e}")
197
+
198
+ def _generate_schema(self, function: Callable) -> Dict[str, Any]:
199
+ """
200
+ Generate a basic JSON schema from function signature.
201
+
202
+ Args:
203
+ function: Function to generate schema for
204
+
205
+ Returns:
206
+ JSON schema dictionary
207
+ """
208
+ sig = inspect.signature(function)
209
+ properties = {}
210
+ required = []
211
+
212
+ for param_name, param in sig.parameters.items():
213
+ # Skip self, cls, context parameters
214
+ if param_name in ['self', 'cls', 'ctx', 'context']:
215
+ continue
216
+
217
+ # Basic type inference
218
+ param_type = "string" # default
219
+ if param.annotation != inspect.Parameter.empty:
220
+ if param.annotation in (int, float):
221
+ param_type = "number"
222
+ elif param.annotation == bool:
223
+ param_type = "boolean"
224
+ elif param.annotation in (list, List):
225
+ param_type = "array"
226
+ elif param.annotation in (dict, Dict):
227
+ param_type = "object"
228
+
229
+ properties[param_name] = {"type": param_type}
230
+
231
+ # Required if no default value
232
+ if param.default == inspect.Parameter.empty:
233
+ required.append(param_name)
234
+
235
+ return {
236
+ "type": "object",
237
+ "properties": properties,
238
+ "required": required,
239
+ "additionalProperties": False,
240
+ }
241
+
242
+ def _create_output_type_from_schema(self, output_schema: OutputTypeSchema) -> Type[BaseModel]:
243
+ """
244
+ Dynamically create a Pydantic model from an OutputTypeSchema.
245
+
246
+ Args:
247
+ output_schema: The output type schema from YAML configuration
248
+
249
+ Returns:
250
+ A dynamically created Pydantic model class
251
+
252
+ Example YAML that creates the model:
253
+ output_type:
254
+ name: "SQLAgentOutput"
255
+ description: "Output for SQL agent"
256
+ fields:
257
+ answer:
258
+ type: str
259
+ description: "The answer"
260
+ sql_query:
261
+ type: str
262
+ description: "The SQL query"
263
+ default: ""
264
+ """
265
+ # Map string type names to Python types
266
+ type_mapping = {
267
+ 'str': str,
268
+ 'string': str,
269
+ 'int': int,
270
+ 'integer': int,
271
+ 'float': float,
272
+ 'number': float,
273
+ 'bool': bool,
274
+ 'boolean': bool,
275
+ 'list': list,
276
+ 'array': list,
277
+ 'dict': dict,
278
+ 'object': dict,
279
+ }
280
+
281
+ # Build field definitions for create_model
282
+ field_definitions = {}
283
+
284
+ for field_name, field_config in output_schema.fields.items():
285
+ # Get the Python type
286
+ python_type = type_mapping.get(field_config.type.lower(), str)
287
+
288
+ # Determine default value
289
+ if field_config.default is not None:
290
+ default_value = field_config.default
291
+ elif field_config.default_factory:
292
+ # Handle default_factory for mutable types
293
+ if field_config.default_factory.lower() in ('list', 'array', '[]'):
294
+ default_value = Field(default_factory=list, description=field_config.description)
295
+ field_definitions[field_name] = (python_type, default_value)
296
+ continue
297
+ elif field_config.default_factory.lower() in ('dict', 'object', '{}'):
298
+ default_value = Field(default_factory=dict, description=field_config.description)
299
+ field_definitions[field_name] = (python_type, default_value)
300
+ continue
301
+ else:
302
+ default_value = ... # Required field
303
+ elif not field_config.required:
304
+ # Optional field with None default
305
+ default_value = None
306
+ else:
307
+ # Required field
308
+ default_value = ...
309
+
310
+ # Create Field with description
311
+ if default_value is ...:
312
+ field_definitions[field_name] = (
313
+ python_type,
314
+ Field(description=field_config.description)
315
+ )
316
+ else:
317
+ field_definitions[field_name] = (
318
+ python_type,
319
+ Field(default=default_value, description=field_config.description)
320
+ )
321
+
322
+ # Create the dynamic model
323
+ dynamic_model = create_model(
324
+ output_schema.name,
325
+ __doc__=output_schema.description or f"Dynamically generated output model: {output_schema.name}",
326
+ **field_definitions
327
+ )
328
+
329
+ return dynamic_model
330
+
331
+ def _resolve_output_type(self, output_type_config: Optional[str | OutputTypeSchema]) -> Optional[Type]:
332
+ """
333
+ Resolve output_type configuration to a Python class.
334
+
335
+ Args:
336
+ output_type_config: Either a string class path or an OutputTypeSchema
337
+
338
+ Returns:
339
+ A Pydantic model class or None
340
+ """
341
+ if output_type_config is None:
342
+ return None
343
+
344
+ if isinstance(output_type_config, str):
345
+ # It's a class path, load it
346
+ return self._load_class(output_type_config)
347
+ elif isinstance(output_type_config, OutputTypeSchema):
348
+ # It's an inline schema, create the model dynamically
349
+ return self._create_output_type_from_schema(output_type_config)
350
+ else:
351
+ raise ValueError(f"Invalid output_type configuration: {type(output_type_config)}")
352
+
353
+ def resolve_prompt_config(
354
+ self,
355
+ prompt_ref: str | PromptConfigSchema,
356
+ config: Optional[ConfigFileSchema] = None
357
+ ) -> PromptConfigSchema:
358
+ """
359
+ Resolve a prompt configuration reference.
360
+
361
+ Args:
362
+ prompt_ref: Prompt name or full configuration
363
+ config: Configuration to use (uses loaded config if None)
364
+
365
+ Returns:
366
+ Resolved prompt configuration
367
+ """
368
+ if isinstance(prompt_ref, PromptConfigSchema):
369
+ return prompt_ref
370
+
371
+ config = config or self.config
372
+ if config is None:
373
+ raise ValueError("No configuration loaded")
374
+
375
+ if prompt_ref not in config.prompts:
376
+ raise ValueError(f"Prompt '{prompt_ref}' not found in configuration")
377
+
378
+ return config.prompts[prompt_ref]
379
+
380
+ def resolve_tools(
381
+ self,
382
+ tool_refs: List[str | ToolConfigSchema],
383
+ config: Optional[ConfigFileSchema] = None
384
+ ) -> List[ToolConfigSchema]:
385
+ """
386
+ Resolve tool configuration references.
387
+
388
+ Args:
389
+ tool_refs: List of tool names or full configurations
390
+ config: Configuration to use (uses loaded config if None)
391
+
392
+ Returns:
393
+ List of resolved tool configurations
394
+ """
395
+ config = config or self.config
396
+ if config is None:
397
+ raise ValueError("No configuration loaded")
398
+
399
+ resolved = []
400
+ for ref in tool_refs:
401
+ if isinstance(ref, ToolConfigSchema):
402
+ resolved.append(ref)
403
+ elif isinstance(ref, str):
404
+ if ref not in config.tools:
405
+ raise ValueError(f"Tool '{ref}' not found in configuration")
406
+ resolved.append(config.tools[ref])
407
+ else:
408
+ raise ValueError(f"Invalid tool reference: {ref}")
409
+
410
+ return resolved
411
+
412
+ def create_agent(
413
+ self,
414
+ agent_name: str,
415
+ config: Optional[ConfigFileSchema] = None,
416
+ agent_class: Optional[Type] = None,
417
+ ) -> Any:
418
+ """
419
+ Create an agent instance from configuration.
420
+
421
+ Args:
422
+ agent_name: Name of agent in configuration
423
+ config: Configuration to use (uses loaded config if None)
424
+ agent_class: Agent class to instantiate (imports BaseAgent if None)
425
+
426
+ Returns:
427
+ Instantiated agent
428
+ """
429
+ config = config or self.config
430
+ if config is None:
431
+ raise ValueError("No configuration loaded. Call load_from_yaml or load_from_dict first.")
432
+
433
+ if agent_name not in config.agents:
434
+ raise ValueError(f"Agent '{agent_name}' not found in configuration")
435
+
436
+ agent_config = config.agents[agent_name]
437
+
438
+ # Import agent class if not provided
439
+ if agent_class is None:
440
+ from rakam_system_agent.components import BaseAgent
441
+ agent_class = BaseAgent
442
+
443
+ # Resolve prompt config
444
+ prompt_config = self.resolve_prompt_config(agent_config.prompt_config, config)
445
+
446
+ # Resolve tools
447
+ tool_configs = self.resolve_tools(agent_config.tools, config)
448
+
449
+ # Create tool registry with resolved tools
450
+ registry = ToolRegistry()
451
+ for tool_config in tool_configs:
452
+ self._register_tool(registry, tool_config)
453
+
454
+ # Load deps_type if specified
455
+ deps_type = None
456
+ if agent_config.deps_type:
457
+ deps_type = self._load_class(agent_config.deps_type)
458
+
459
+ # Resolve output_type (can be string class path or inline schema)
460
+ output_type = self._resolve_output_type(agent_config.output_type)
461
+
462
+ # Convert model config to ModelSettings
463
+ model_settings = ModelSettings(
464
+ parallel_tool_calls=agent_config.llm_config.parallel_tool_calls,
465
+ temperature=agent_config.llm_config.temperature,
466
+ max_tokens=agent_config.llm_config.max_tokens,
467
+ **agent_config.llm_config.extra_settings,
468
+ )
469
+
470
+ # Create agent configuration dict
471
+ config_dict = {
472
+ "model": agent_config.llm_config.model,
473
+ "stateful": agent_config.stateful,
474
+ "system_prompt": prompt_config.system_prompt,
475
+ **agent_config.metadata,
476
+ }
477
+
478
+ # Instantiate agent
479
+ agent = agent_class(
480
+ name=agent_config.name,
481
+ config=config_dict,
482
+ model=agent_config.llm_config.model,
483
+ deps_type=deps_type,
484
+ output_type=output_type,
485
+ system_prompt=prompt_config.system_prompt,
486
+ tool_registry=registry,
487
+ )
488
+
489
+ # Store tracking configuration
490
+ agent._tracking_enabled = agent_config.enable_tracking
491
+ agent._tracking_output_dir = agent_config.tracking_output_dir
492
+
493
+ return agent
494
+
495
+ def create_all_agents(
496
+ self,
497
+ config: Optional[ConfigFileSchema] = None,
498
+ agent_class: Optional[Type] = None,
499
+ ) -> Dict[str, Any]:
500
+ """
501
+ Create all agents from configuration.
502
+
503
+ Args:
504
+ config: Configuration to use (uses loaded config if None)
505
+ agent_class: Agent class to instantiate (imports BaseAgent if None)
506
+
507
+ Returns:
508
+ Dictionary mapping agent names to instances
509
+ """
510
+ config = config or self.config
511
+ if config is None:
512
+ raise ValueError("No configuration loaded")
513
+
514
+ agents = {}
515
+ for agent_name in config.agents:
516
+ agents[agent_name] = self.create_agent(agent_name, config, agent_class)
517
+
518
+ return agents
519
+
520
+ def validate_config(self, config_path: Optional[str] = None) -> tuple[bool, List[str]]:
521
+ """
522
+ Validate a configuration file.
523
+
524
+ Args:
525
+ config_path: Path to config file (uses loaded config if None)
526
+
527
+ Returns:
528
+ Tuple of (is_valid, list_of_errors)
529
+ """
530
+ errors = []
531
+
532
+ try:
533
+ if config_path:
534
+ config = self.load_from_yaml(config_path)
535
+ else:
536
+ config = self.config
537
+ if config is None:
538
+ return False, ["No configuration loaded"]
539
+
540
+ # Validate agent references
541
+ for agent_name, agent_config in config.agents.items():
542
+ # Check prompt reference
543
+ try:
544
+ self.resolve_prompt_config(agent_config.prompt_config, config)
545
+ except ValueError as e:
546
+ errors.append(f"Agent '{agent_name}': {e}")
547
+
548
+ # Check tool references
549
+ try:
550
+ self.resolve_tools(agent_config.tools, config)
551
+ except ValueError as e:
552
+ errors.append(f"Agent '{agent_name}': {e}")
553
+
554
+ # Check deps_type if specified
555
+ if agent_config.deps_type:
556
+ try:
557
+ self._load_class(agent_config.deps_type)
558
+ except (ImportError, AttributeError) as e:
559
+ errors.append(f"Agent '{agent_name}' deps_type: {e}")
560
+
561
+ # Check output_type if specified
562
+ if agent_config.output_type:
563
+ try:
564
+ self._resolve_output_type(agent_config.output_type)
565
+ except (ImportError, AttributeError, ValueError) as e:
566
+ errors.append(f"Agent '{agent_name}' output_type: {e}")
567
+
568
+ # Validate tools can be loaded
569
+ for tool_name, tool_config in config.tools.items():
570
+ if tool_config.type == ToolMode.DIRECT:
571
+ try:
572
+ self._load_function(tool_config.module, tool_config.function)
573
+ except (ImportError, AttributeError) as e:
574
+ errors.append(f"Tool '{tool_name}': {e}")
575
+
576
+ return len(errors) == 0, errors
577
+
578
+ except Exception as e:
579
+ return False, [f"Configuration error: {e}"]
580
+