Skip to content

Commit d3a25ea

Browse files
Merge pull request #80 from multimindlab/medium_issues_solved
Fix routing reliability, kwargs safety, async session reuse, and secu…
2 parents 8ea4158 + b91ff40 commit d3a25ea

34 files changed

Lines changed: 573 additions & 246 deletions

multimind/agents/agent_loader.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ class AgentLoader:
1616
def __init__(self, model_registry: Optional[Dict[str, BaseLLM]] = None):
1717
self.model_registry = model_registry or {}
1818
self.tool_registry: Dict[str, BaseTool] = {}
19+
self._base_dir = Path.cwd().resolve()
20+
21+
def _resolve_safe_path(self, path: str) -> Path:
22+
"""Resolve and validate path stays under the loader base directory."""
23+
candidate = Path(path).expanduser()
24+
resolved = candidate.resolve()
25+
try:
26+
resolved.relative_to(self._base_dir)
27+
except ValueError as e:
28+
raise ValueError(
29+
f"Path traversal detected. '{path}' escapes base directory '{self._base_dir}'."
30+
) from e
31+
return resolved
1932

2033
def register_model(self, name: str, model: BaseLLM) -> None:
2134
"""Register a model for use in agents."""
@@ -32,23 +45,27 @@ def load_agent(
3245
tools: Optional[List[BaseTool]] = None
3346
) -> Agent:
3447
"""Load an agent from a configuration file."""
48+
safe_config_path = self._resolve_safe_path(config_path)
49+
if safe_config_path.suffix.lower() != ".json":
50+
raise ValueError(f"Agent config must be a JSON file: {config_path}")
51+
3552
# Load config
3653
try:
37-
with open(config_path, "r", encoding="utf-8") as f:
54+
with open(safe_config_path, "r", encoding="utf-8") as f:
3855
config = json.load(f)
3956
except FileNotFoundError as e:
40-
raise FileNotFoundError(f"Agent config file not found: {config_path}") from e
57+
raise FileNotFoundError(f"Agent config file not found: {safe_config_path}") from e
4158
except json.JSONDecodeError as e:
4259
raise ValueError(
43-
f"Invalid JSON in agent config file: {config_path}. {e}"
60+
f"Invalid JSON in agent config file: {safe_config_path}. {e}"
4461
) from e
4562
except OSError as e:
4663
raise RuntimeError(
47-
f"Failed to read agent config file: {config_path}. {e}"
64+
f"Failed to read agent config file: {safe_config_path}. {e}"
4865
) from e
4966

5067
if not isinstance(config, dict):
51-
raise ValueError(f"Agent config must be a JSON object: {config_path}")
68+
raise ValueError(f"Agent config must be a JSON object: {safe_config_path}")
5269

5370
# Validate config
5471
required_keys = {"model", "system_prompt"}
@@ -93,7 +110,9 @@ def load_agents_from_dir(
93110
) -> Dict[str, Agent]:
94111
"""Load multiple agents from a directory of config files."""
95112
agents = {}
96-
config_dir = Path(dir_path)
113+
config_dir = self._resolve_safe_path(dir_path)
114+
if not config_dir.is_dir():
115+
raise FileNotFoundError(f"Agent config directory not found: {config_dir}")
97116

98117
for config_file in config_dir.glob("*.json"):
99118
agent_name = config_file.stem

multimind/agents/memory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ def get_history(self, n: Optional[int] = None) -> List[Dict[str, Any]]:
6565
"task": task,
6666
"response": response,
6767
# Prefer response timestamp because it reflects when the completion arrived.
68-
"timestamp": resp_ts.isoformat() if isinstance(resp_ts, datetime) else datetime.now().isoformat(),
68+
"timestamp": resp_ts.isoformat() if isinstance(resp_ts, datetime) else None,
6969
"task_timestamp": task_ts.isoformat() if isinstance(task_ts, datetime) else None,
7070
})
7171
return history

multimind/agents/prompt_correction.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,17 @@ def monitor(self, prompt: str, output: str, trace: Dict = None) -> str:
3030
self.logger.warning(f"Detected issue in output: {output}")
3131
for hook in self.error_hooks:
3232
hook(prompt, Exception("Detected hallucination"), trace)
33-
# Apply correction hooks
33+
# Apply correction hooks to the *output* and return corrected output.
34+
# (Correction hooks are expected to take a string and trace, and return a string.)
35+
corrected_output = output
3436
for hook in self.correction_hooks:
35-
prompt = hook(prompt, trace)
36-
self.logger.info(f"Corrected prompt: {prompt}")
37-
return prompt
38-
return prompt
37+
corrected_output = hook(corrected_output, trace)
38+
self.logger.info(f"Corrected output: {corrected_output}")
39+
return corrected_output
40+
return output
3941
except Exception as e:
4042
self.logger.error(f"Prompt correction failed: {e}")
41-
return prompt
43+
return output
4244

4345
def update_adapter(self, adapter_key: str, new_adapter_path: str):
4446
"""
@@ -61,6 +63,6 @@ def adapter_updater(adapter_key, new_path):
6163
pcl.add_correction_hook(simple_correction)
6264
pcl.add_adapter_update_hook(adapter_updater)
6365
# Simulate monitoring
64-
new_prompt = pcl.monitor("What is the capital of France?", "[error] hallucination detected", {"step": 1})
65-
print("New prompt after correction:", new_prompt)
66+
corrected_output = pcl.monitor("What is the capital of France?", "[error] hallucination detected", {"step": 1})
67+
print("Corrected output after correction:", corrected_output)
6668
pcl.update_adapter("user123", "lora_adapter_v2")

multimind/agents/react_toolchain.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
from typing import List, Callable, Any, Dict
22

3+
4+
class ReasoningChainExecutionError(RuntimeError):
5+
"""Raised when a reasoning chain step fails."""
6+
7+
38
class ReasoningStep:
49
"""
510
Represents a single step in a reasoning/toolchain. Can be a model call, tool call, or custom function.
@@ -21,13 +26,29 @@ def __init__(self, steps: List[ReasoningStep]):
2126
self.hooks = [] # List of callables: hook(step, input, output)
2227
def add_hook(self, hook: Callable[[ReasoningStep, Any, Any], None]):
2328
self.hooks.append(hook)
29+
2430
def run(self, input_data: Any, context: Dict = None):
2531
context = context or {}
2632
data = input_data
2733
for step in self.steps:
28-
output = step(data, context=context)
34+
try:
35+
output = step(data, context=context)
36+
except Exception as e:
37+
context["last_failed_step"] = step.name
38+
context.setdefault("errors", []).append(
39+
{"step": step.name, "error": str(e)}
40+
)
41+
raise ReasoningChainExecutionError(
42+
f"Reasoning chain failed at step '{step.name}': {e}"
43+
) from e
44+
2945
for hook in self.hooks:
30-
hook(step, data, output)
46+
try:
47+
hook(step, data, output)
48+
except Exception as e:
49+
context.setdefault("hook_errors", []).append(
50+
{"step": step.name, "hook": getattr(hook, "__name__", str(hook)), "error": str(e)}
51+
)
3152
data = output
3253
return data
3354

multimind/agents/tools/calculator.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
class CalculatorTool(BaseTool):
1212
"""A tool for performing mathematical calculations."""
1313

14+
MAX_EXPRESSION_LENGTH = 512
15+
MAX_AST_NODES = 128
16+
MAX_AST_DEPTH = 24
17+
1418
def __init__(self):
1519
super().__init__(
1620
name="calculator",
@@ -53,12 +57,25 @@ def get_parameters(self) -> Dict[str, Any]:
5357

5458
def _evaluate(self, expression: str) -> Union[int, float]:
5559
"""Safely evaluate a mathematical expression."""
60+
if len(expression) > self.MAX_EXPRESSION_LENGTH:
61+
raise ValueError(
62+
f"Expression too long (max {self.MAX_EXPRESSION_LENGTH} characters)"
63+
)
64+
65+
def _ast_depth(node: ast.AST) -> int:
66+
children = list(ast.iter_child_nodes(node))
67+
if not children:
68+
return 1
69+
return 1 + max(_ast_depth(child) for child in children)
70+
5671
def _eval(node) -> Union[int, float]:
57-
if isinstance(node, ast.Num):
58-
val = node.n
72+
if isinstance(node, ast.Constant):
73+
val = node.value
5974
if isinstance(val, complex):
6075
raise ValueError("Complex numbers are not supported")
61-
return float(val) if isinstance(val, Real) else val
76+
if not isinstance(val, Real):
77+
raise TypeError(f"Unsupported constant type: {type(val)}")
78+
return float(val)
6279
elif isinstance(node, ast.BinOp):
6380
return self.operators[type(node.op)](
6481
_eval(node.left),
@@ -70,6 +87,12 @@ def _eval(node) -> Union[int, float]:
7087
raise TypeError(f"Unsupported operation: {type(node)}")
7188

7289
tree = ast.parse(expression, mode='eval')
90+
node_count = sum(1 for _ in ast.walk(tree))
91+
if node_count > self.MAX_AST_NODES:
92+
raise ValueError(f"Expression too complex (max {self.MAX_AST_NODES} AST nodes)")
93+
if _ast_depth(tree) > self.MAX_AST_DEPTH:
94+
raise ValueError(f"Expression too deeply nested (max depth {self.MAX_AST_DEPTH})")
95+
7396
result = _eval(tree.body)
7497
if isinstance(result, complex):
7598
raise ValueError("Complex numbers are not supported")

0 commit comments

Comments
 (0)