Skip to content

Commit 2152c66

Browse files
committed
Fix Streaming, Memory, SQLAlchemy Cleanup and Reliability Issues Across SDK
1 parent d3a25ea commit 2152c66

55 files changed

Lines changed: 634 additions & 261 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

multimind/agents/agent_loader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def load_agent(
9393
max_history=memory_config.get("max_history", 100)
9494
)
9595

96-
# Create agen
96+
# Create agent
9797
agent = Agent(
9898
model=model,
9999
memory=memory,

multimind/agents/prompt_correction.py

Lines changed: 50 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from typing import Callable, Any, Dict, List
22
import logging
33

4+
logger = logging.getLogger(__name__)
5+
6+
47
class PromptCorrectionLayer:
58
"""
69
Observability and self-healing layer for LLM/agent pipelines.
@@ -19,23 +22,61 @@ def add_correction_hook(self, hook: Callable[[str, Dict], str]):
1922
def add_adapter_update_hook(self, hook: Callable[[str, str], None]):
2023
self.adapter_update_hooks.append(hook)
2124

25+
def _compute_issue_score(self, prompt: str, output: str, trace: Dict) -> float:
26+
"""
27+
Heuristic scoring function for potential issues / hallucinations.
28+
Returns a score in [0, 1], where higher means more suspicious.
29+
"""
30+
text = output.lower()
31+
score = 0.0
32+
33+
# Strong indicators
34+
strong_markers = [
35+
"[error]", "hallucination", "not based on real data",
36+
"fabricated answer", "made this up"
37+
]
38+
if any(marker in text for marker in strong_markers):
39+
score += 0.7
40+
41+
# Weaker indicators based on uncertainty phrases
42+
weak_markers = [
43+
"i am not sure", "i'm not sure", "i do not know",
44+
"i don't know", "cannot verify", "not certain"
45+
]
46+
if any(marker in text for marker in weak_markers):
47+
score += 0.2
48+
49+
# If trace provides an explicit model_score / confidence, incorporate it.
50+
# Expecting trace.get("confidence") in [0, 1] where low is suspicious.
51+
confidence = trace.get("confidence")
52+
if isinstance(confidence, (int, float)):
53+
confidence_clamped = max(0.0, min(1.0, float(confidence)))
54+
score += (1.0 - confidence_clamped) * 0.3
55+
56+
return min(score, 1.0)
57+
2258
def monitor(self, prompt: str, output: str, trace: Dict = None) -> str:
2359
"""
2460
Monitor output for errors/hallucinations and apply corrections if needed.
61+
Uses a heuristic score instead of a single string check.
2562
"""
2663
trace = trace or {}
2764
try:
28-
# Example: simple hallucination check (can be replaced with real logic)
29-
if "[error]" in output or "hallucination" in output.lower():
30-
self.logger.warning(f"Detected issue in output: {output}")
65+
issue_score = self._compute_issue_score(prompt, output, trace)
66+
threshold = trace.get("hallucination_threshold", 0.6)
67+
if issue_score >= threshold:
68+
self.logger.warning(
69+
"Detected potential hallucination (score=%.2f, threshold=%.2f): %s",
70+
issue_score,
71+
threshold,
72+
output,
73+
)
3174
for hook in self.error_hooks:
3275
hook(prompt, Exception("Detected hallucination"), trace)
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.)
3576
corrected_output = output
3677
for hook in self.correction_hooks:
3778
corrected_output = hook(corrected_output, trace)
38-
self.logger.info(f"Corrected output: {corrected_output}")
79+
self.logger.info("Corrected output: %s", corrected_output)
3980
return corrected_output
4081
return output
4182
except Exception as e:
@@ -54,15 +95,15 @@ def update_adapter(self, adapter_key: str, new_adapter_path: str):
5495
if __name__ == "__main__":
5596
pcl = PromptCorrectionLayer()
5697
def error_logger(prompt, exc, trace):
57-
print(f"Error detected for prompt '{prompt}': {exc}")
98+
logger.error("Error detected for prompt '%s': %s", prompt, exc)
5899
def simple_correction(prompt, trace):
59100
return prompt + " [CORRECTED]"
60101
def adapter_updater(adapter_key, new_path):
61-
print(f"Adapter {adapter_key} updated to {new_path}")
102+
logger.info("Adapter %s updated to %s", adapter_key, new_path)
62103
pcl.add_error_hook(error_logger)
63104
pcl.add_correction_hook(simple_correction)
64105
pcl.add_adapter_update_hook(adapter_updater)
65106
# Simulate monitoring
66107
corrected_output = pcl.monitor("What is the capital of France?", "[error] hallucination detected", {"step": 1})
67-
print("Corrected output after correction:", corrected_output)
108+
logger.info("Corrected output after correction: %s", corrected_output)
68109
pcl.update_adapter("user123", "lora_adapter_v2")

multimind/api/multi_model_api.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
"""
44

55
import logging
6-
from fastapi import FastAPI, HTTPException
6+
import os
7+
from fastapi import FastAPI, HTTPException, Depends, Header
78
from pydantic import BaseModel, Field
89
from typing import List, Dict, Optional, Union
910
import asyncio
@@ -16,6 +17,19 @@
1617
app = FastAPI(title="Multi-Model API")
1718
logger = logging.getLogger(__name__)
1819

20+
API_KEYS = os.getenv("API_KEYS", "").split(",") if os.getenv("API_KEYS") else []
21+
22+
23+
def verify_api_key(api_key: Optional[str] = Header(None, alias="X-API-Key")) -> bool:
24+
"""Verify the API key from request header."""
25+
if not API_KEYS:
26+
return True
27+
if not api_key:
28+
raise HTTPException(status_code=401, detail="API key required")
29+
if api_key not in API_KEYS:
30+
raise HTTPException(status_code=401, detail="Invalid API key")
31+
return True
32+
1933
# Reuse a single factory across requests to avoid re-loading env / re-allocating caches.
2034
_MODEL_FACTORY = ModelFactory()
2135

@@ -79,7 +93,7 @@ class EmbeddingsRequest(BaseModel):
7993
model_weights: Optional[Dict[str, float]] = None
8094

8195
@app.post("/generate")
82-
async def generate(request: GenerateRequest):
96+
async def generate(request: GenerateRequest, authenticated: bool = Depends(verify_api_key)):
8397
"""Generate text using the multi-model wrapper."""
8498
try:
8599
multi_model = await _get_multi_model(
@@ -99,7 +113,7 @@ async def generate(request: GenerateRequest):
99113
raise HTTPException(status_code=500, detail="Internal server error")
100114

101115
@app.post("/chat")
102-
async def chat(request: ChatRequest):
116+
async def chat(request: ChatRequest, authenticated: bool = Depends(verify_api_key)):
103117
"""Generate chat completion using the multi-model wrapper."""
104118
try:
105119
multi_model = await _get_multi_model(
@@ -119,7 +133,7 @@ async def chat(request: ChatRequest):
119133
raise HTTPException(status_code=500, detail="Internal server error")
120134

121135
@app.post("/embeddings")
122-
async def embeddings(request: EmbeddingsRequest):
136+
async def embeddings(request: EmbeddingsRequest, authenticated: bool = Depends(verify_api_key)):
123137
"""Generate embeddings using the multi-model wrapper."""
124138
try:
125139
multi_model = await _get_multi_model(

multimind/api/unified_api.py

Lines changed: 46 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
Unified API endpoint for multi-modal processing with MoE support.
33
"""
44

5-
from fastapi import FastAPI, HTTPException
5+
from fastapi import FastAPI, HTTPException, Depends, Header
66
from pydantic import BaseModel, Field
77
from typing import Dict, List, Any, Optional, Union
88
import asyncio
@@ -19,9 +19,41 @@
1919

2020
app = FastAPI(title="Unified Multi-Modal API")
2121

22+
API_KEYS = os.getenv("API_KEYS", "").split(",") if os.getenv("API_KEYS") else []
23+
24+
25+
def verify_api_key(api_key: Optional[str] = Header(None, alias="X-API-Key")) -> bool:
26+
"""Verify the API key from request header."""
27+
if not API_KEYS:
28+
return True
29+
if not api_key:
30+
raise HTTPException(status_code=401, detail="API key required")
31+
if api_key not in API_KEYS:
32+
raise HTTPException(status_code=401, detail="Invalid API key")
33+
return True
34+
2235
# Reuse a single factory across requests to avoid re-creating model caches.
2336
_MODEL_FACTORY = ModelFactory()
2437

38+
_ROUTER = None
39+
_WORKFLOW_REGISTRY = None
40+
41+
42+
def _get_router():
43+
global _ROUTER
44+
if _ROUTER is None:
45+
from ..router.multi_modal_router import MultiModalRouter
46+
_ROUTER = MultiModalRouter()
47+
return _ROUTER
48+
49+
50+
def _get_workflow_registry():
51+
global _WORKFLOW_REGISTRY
52+
if _WORKFLOW_REGISTRY is None:
53+
from .mcp.registry import WorkflowRegistry
54+
_WORKFLOW_REGISTRY = WorkflowRegistry()
55+
return _WORKFLOW_REGISTRY
56+
2557

2658
class _TextExpertAdapter(Expert):
2759
"""Expert wrapper around a model instance for text."""
@@ -169,16 +201,13 @@ def _build_experts(modalities: List[str], router: Any) -> Dict[str, Expert]:
169201
return experts
170202

171203
@app.post("/v1/process", response_model=UnifiedResponse)
172-
async def process_request(request: UnifiedRequest):
204+
async def process_request(request: UnifiedRequest, authenticated: bool = Depends(verify_api_key)):
173205
"""Process multi-modal request using either MoE or router."""
174206
try:
175-
# Import here to avoid circular imports
176-
from ..router.multi_modal_router import MultiModalRouter, MultiModalRequest
177-
from .mcp.registry import WorkflowRegistry
178-
179-
# Initialize components
180-
router = MultiModalRouter()
181-
workflow_registry = WorkflowRegistry()
207+
from ..router.multi_modal_router import MultiModalRequest
208+
209+
router = _get_router()
210+
workflow_registry = _get_workflow_registry()
182211

183212
# Convert inputs to router format (support multiple inputs per modality)
184213
content: Dict[str, Any] = {}
@@ -288,32 +317,26 @@ async def process_request(request: UnifiedRequest):
288317
raise HTTPException(status_code=500, detail="Internal server error")
289318

290319
@app.get("/v1/models")
291-
async def list_models():
320+
async def list_models(authenticated: bool = Depends(verify_api_key)):
292321
"""List available models and their capabilities."""
293-
# Import here to avoid circular imports
294-
from ..router.multi_modal_router import MultiModalRouter
295-
router = MultiModalRouter()
296-
322+
router = _get_router()
323+
297324
models = {}
298325
for modality, model_dict in router.modality_registry.items():
299326
models[modality] = list(model_dict.keys())
300327
return {"models": models}
301328

302329
@app.get("/v1/workflows")
303-
async def list_workflows():
330+
async def list_workflows(authenticated: bool = Depends(verify_api_key)):
304331
"""List available MCP workflows."""
305-
# Import here to avoid circular imports
306-
from .mcp.registry import WorkflowRegistry
307-
workflow_registry = WorkflowRegistry()
332+
workflow_registry = _get_workflow_registry()
308333
return {"workflows": workflow_registry.list_workflows()}
309334

310335
@app.get("/v1/metrics")
311-
async def get_metrics():
336+
async def get_metrics(authenticated: bool = Depends(verify_api_key)):
312337
"""Get performance metrics for models."""
313-
# Import here to avoid circular imports
314-
from ..router.multi_modal_router import MultiModalRouter
315-
router = MultiModalRouter()
316-
338+
router = _get_router()
339+
317340
return {
318341
"costs": router.cost_tracker.costs,
319342
"performance": router.performance_metrics.metrics

multimind/client/federated_router.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
from typing import Callable, Dict, Any
2+
import logging
23
import time
34

5+
logger = logging.getLogger(__name__)
6+
7+
48
class FederatedRouter:
59
"""
610
Routes between local (on-device) and cloud model clients based on context (input size, latency, privacy, etc.).
@@ -41,5 +45,5 @@ def generate(self, prompt, **kwargs):
4145
local = DummyClient()
4246
cloud = DummyClient()
4347
router = FederatedRouter(local, cloud)
44-
print(router.generate("short prompt"))
45-
print(router.generate("This is a very long prompt that should go to the cloud..." * 20))
48+
logger.info("%s", router.generate("short prompt"))
49+
logger.info("%s", router.generate("This is a very long prompt that should go to the cloud..." * 20))

multimind/compliance/advanced.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ def update_epsilon(self, epsilon: float):
4242
from datetime import datetime
4343
import json
4444
import asyncio
45+
import hashlib
4546
from pathlib import Path
4647
from dataclasses import dataclass
4748
from enum import Enum
@@ -291,11 +292,12 @@ async def check_and_heal(self, compliance_state: Dict[str, Any]) -> Dict[str, An
291292

292293
def _get_state_metadata(self, state: Dict[str, Any]) -> Dict[str, Any]:
293294
"""Get metadata for a compliance state."""
295+
state_bytes = json.dumps(state, sort_keys=True, default=str).encode("utf-8")
294296
return {
295297
"status": state.get("status", "unknown"),
296298
"timestamp": datetime.now().isoformat(),
297299
"version": state.get("version", "1.0"),
298-
"checksum": hash(str(state))
300+
"checksum": hashlib.sha256(state_bytes).hexdigest()
299301
}
300302

301303
def _create_rollback_point(self, state: Dict[str, Any]):
@@ -497,8 +499,13 @@ async def _extract_watermark(self, model: Any) -> str:
497499

498500
async def _generate_fingerprint(self, model: Any) -> str:
499501
"""Generate fingerprint for the model."""
500-
# Placeholder implementation: Replace with actual fingerprint generation logic
501-
return f"fingerprint_{hash(str(model))}"
502+
# Deterministic cryptographic fingerprint for model identity.
503+
model_payload = {
504+
"type": type(model).__name__,
505+
"repr": repr(model),
506+
}
507+
model_bytes = json.dumps(model_payload, sort_keys=True, default=str).encode("utf-8")
508+
return f"fingerprint_{hashlib.sha256(model_bytes).hexdigest()}"
502509

503510
async def verify_watermark(self, model) -> Dict[str, Any]:
504511
"""Enhanced watermark verification with tamper detection."""
@@ -533,10 +540,17 @@ async def track_fingerprint(self, model: Any) -> Dict[str, Any]:
533540
"""Track and return fingerprint information for a model."""
534541
fingerprint = await self._generate_fingerprint(model)
535542
await self.fingerprint_tracker.track(fingerprint)
543+
model_id = hashlib.sha256(
544+
json.dumps(
545+
{"type": type(model).__name__, "repr": repr(model)},
546+
sort_keys=True,
547+
default=str
548+
).encode("utf-8")
549+
).hexdigest()[:16]
536550
return {
537551
"fingerprint": fingerprint,
538552
"timestamp": datetime.now().isoformat(),
539-
"model_id": str(hash(str(model)))
553+
"model_id": model_id
540554
}
541555

542556
class AdaptivePrivacy:

multimind/compliance/advanced_config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ class ComplianceShardConfig(BaseModel):
4040
jurisdiction: str
4141
epsilon: float = 1.0
4242
rules: List[Dict[str, Any]]
43-
metadata: Dict[str, Any] = {}
43+
metadata: Dict[str, Any] = Field(default_factory=dict)
4444
compliance_level: ComplianceLevel = ComplianceLevel.STANDARD
4545
encryption_enabled: bool = True
4646
metrics_tracking: bool = True

multimind/compliance/audit.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -129,16 +129,16 @@ async def cleanup_old_events(self) -> int:
129129

130130
async def export_events(
131131
self,
132-
format: str = "json",
132+
export_format: str = "json",
133133
start_time: Optional[datetime] = None,
134134
end_time: Optional[datetime] = None
135135
) -> str:
136136
"""Export audit events in specified format."""
137137
events = await self.get_events(start_time=start_time, end_time=end_time)
138138

139-
if format == "json":
139+
if export_format == "json":
140140
return json.dumps([e.dict() for e in events], default=str)
141-
elif format == "csv":
141+
elif export_format == "csv":
142142
import csv
143143
import io
144144
if not events:
@@ -156,7 +156,7 @@ async def export_events(
156156
writer.writerow(row)
157157
return output.getvalue()
158158
else:
159-
raise ValueError(f"Unsupported export format: {format}")
159+
raise ValueError(f"Unsupported export format: {export_format}")
160160

161161
async def get_compliance_report(
162162
self,

0 commit comments

Comments
 (0)