guardianhub 0.1.88__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.
- guardianhub/__init__.py +29 -0
- guardianhub/_version.py +1 -0
- guardianhub/agents/runtime.py +12 -0
- guardianhub/auth/token_provider.py +22 -0
- guardianhub/clients/__init__.py +2 -0
- guardianhub/clients/classification_client.py +52 -0
- guardianhub/clients/graph_db_client.py +161 -0
- guardianhub/clients/langfuse/dataset_client.py +157 -0
- guardianhub/clients/langfuse/manager.py +118 -0
- guardianhub/clients/langfuse/prompt_client.py +68 -0
- guardianhub/clients/langfuse/score_evaluation_client.py +92 -0
- guardianhub/clients/langfuse/tracing_client.py +250 -0
- guardianhub/clients/langfuse_client.py +63 -0
- guardianhub/clients/llm_client.py +144 -0
- guardianhub/clients/llm_service.py +295 -0
- guardianhub/clients/metadata_extractor_client.py +53 -0
- guardianhub/clients/ocr_client.py +81 -0
- guardianhub/clients/paperless_client.py +515 -0
- guardianhub/clients/registry_client.py +18 -0
- guardianhub/clients/text_cleaner_client.py +58 -0
- guardianhub/clients/vector_client.py +344 -0
- guardianhub/config/__init__.py +0 -0
- guardianhub/config/config_development.json +84 -0
- guardianhub/config/config_prod.json +39 -0
- guardianhub/config/settings.py +221 -0
- guardianhub/http/http_client.py +26 -0
- guardianhub/logging/__init__.py +2 -0
- guardianhub/logging/logging.py +168 -0
- guardianhub/logging/logging_filters.py +35 -0
- guardianhub/models/__init__.py +0 -0
- guardianhub/models/agent_models.py +153 -0
- guardianhub/models/base.py +2 -0
- guardianhub/models/registry/client.py +16 -0
- guardianhub/models/registry/dynamic_loader.py +73 -0
- guardianhub/models/registry/loader.py +37 -0
- guardianhub/models/registry/registry.py +17 -0
- guardianhub/models/registry/signing.py +70 -0
- guardianhub/models/template/__init__.py +0 -0
- guardianhub/models/template/agent_plan.py +65 -0
- guardianhub/models/template/agent_response_evaluation.py +67 -0
- guardianhub/models/template/extraction.py +29 -0
- guardianhub/models/template/reflection_critique.py +206 -0
- guardianhub/models/template/suggestion.py +42 -0
- guardianhub/observability/__init__.py +1 -0
- guardianhub/observability/instrumentation.py +271 -0
- guardianhub/observability/otel_helper.py +43 -0
- guardianhub/observability/otel_middlewares.py +73 -0
- guardianhub/prompts/base.py +7 -0
- guardianhub/prompts/providers/langfuse_provider.py +13 -0
- guardianhub/prompts/providers/local_provider.py +22 -0
- guardianhub/prompts/registry.py +14 -0
- guardianhub/scripts/script.sh +31 -0
- guardianhub/services/base.py +15 -0
- guardianhub/template/__init__.py +0 -0
- guardianhub/tools/gh_registry_cli.py +171 -0
- guardianhub/utils/__init__.py +0 -0
- guardianhub/utils/app_state.py +74 -0
- guardianhub/utils/fastapi_utils.py +152 -0
- guardianhub/utils/json_utils.py +137 -0
- guardianhub/utils/metrics.py +60 -0
- guardianhub-0.1.88.dist-info/METADATA +240 -0
- guardianhub-0.1.88.dist-info/RECORD +64 -0
- guardianhub-0.1.88.dist-info/WHEEL +4 -0
- guardianhub-0.1.88.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# guardianhub_sdk/http/http_client.py
|
|
2
|
+
import httpx
|
|
3
|
+
from typing import Optional, Any, Dict
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class BaseHTTPClient:
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def __init__(self, base_url: str, timeout: int = 30):
|
|
10
|
+
|
|
11
|
+
self.base_url = base_url.rstrip('/')
|
|
12
|
+
self._client = httpx.AsyncClient(timeout=timeout)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def request(self, method: str, path: str, **kwargs) -> httpx.Response:
|
|
16
|
+
url = f"{self.base_url}/{path.lstrip('/')}"
|
|
17
|
+
resp = await self._client.request(method, url, **kwargs)
|
|
18
|
+
resp.raise_for_status()
|
|
19
|
+
return resp
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def get(self, path: str, **kwargs) -> httpx.Response:
|
|
23
|
+
return await self.request("GET", path, **kwargs)
|
|
24
|
+
|
|
25
|
+
async def post(self, path: str, **kwargs) -> httpx.Response:
|
|
26
|
+
return await self.request("POST", path, **kwargs)
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from typing import Optional, Dict, Any, Type, List, Union
|
|
4
|
+
from guardianhub.config import settings
|
|
5
|
+
|
|
6
|
+
# Lazy import to avoid circular imports
|
|
7
|
+
HealthCheckFilter = None
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class LoggerFactory:
|
|
11
|
+
"""Factory class for creating and managing logger instances with consistent configuration."""
|
|
12
|
+
_initialized = False
|
|
13
|
+
_loggers: Dict[str, logging.Logger] = {}
|
|
14
|
+
|
|
15
|
+
@classmethod
|
|
16
|
+
def initialize(cls) -> None:
|
|
17
|
+
"""Initialize the logging configuration if not already done."""
|
|
18
|
+
if not cls._initialized:
|
|
19
|
+
setup_logging()
|
|
20
|
+
cls._initialized = True
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def get_logger(
|
|
24
|
+
cls,
|
|
25
|
+
name: str,
|
|
26
|
+
level: Optional[int] = None,
|
|
27
|
+
extra: Optional[Dict[str, Any]] = None,
|
|
28
|
+
filters: Optional[List[Union[logging.Filter, Type[logging.Filter], dict]]] = None
|
|
29
|
+
) -> logging.Logger:
|
|
30
|
+
"""
|
|
31
|
+
Get or create a logger with the given name and optional configuration.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
name: Logger name (usually __name__ of the calling module)
|
|
35
|
+
level: Optional log level (e.g., logging.INFO, logging.DEBUG)
|
|
36
|
+
extra: Optional dictionary with additional context to add to log records
|
|
37
|
+
filters: Optional list of filter instances, filter classes, or filter config dicts
|
|
38
|
+
(for filters that require initialization)
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Configured logger instance
|
|
42
|
+
"""
|
|
43
|
+
cls.initialize()
|
|
44
|
+
|
|
45
|
+
if name not in cls._loggers:
|
|
46
|
+
logger = logging.getLogger(name)
|
|
47
|
+
if level is not None:
|
|
48
|
+
logger.setLevel(level)
|
|
49
|
+
|
|
50
|
+
# Apply filters if any
|
|
51
|
+
if filters:
|
|
52
|
+
for f in filters:
|
|
53
|
+
if isinstance(f, logging.Filter):
|
|
54
|
+
logger.addFilter(f)
|
|
55
|
+
elif isinstance(f, type) and issubclass(f, logging.Filter):
|
|
56
|
+
logger.addFilter(f())
|
|
57
|
+
elif isinstance(f, dict):
|
|
58
|
+
# For filters that need initialization with parameters
|
|
59
|
+
filter_class = f.pop('class')
|
|
60
|
+
if isinstance(filter_class, type) and issubclass(filter_class, logging.Filter):
|
|
61
|
+
logger.addFilter(filter_class(**f))
|
|
62
|
+
|
|
63
|
+
if extra:
|
|
64
|
+
logger = logging.LoggerAdapter(logger, extra)
|
|
65
|
+
|
|
66
|
+
cls._loggers[name] = logger
|
|
67
|
+
|
|
68
|
+
return cls._loggers[name]
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def get_logger(name: str) -> logging.Logger:
|
|
72
|
+
"""
|
|
73
|
+
Returns a pre-configured logger instance for a given name.
|
|
74
|
+
|
|
75
|
+
This is a convenience wrapper around LoggerFactory.get_logger()
|
|
76
|
+
"""
|
|
77
|
+
return LoggerFactory.get_logger(name)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def setup_logging():
|
|
81
|
+
"""
|
|
82
|
+
Configures the root logger and the uvicorn.access logger using a standard format.
|
|
83
|
+
Applies filters to reduce log noise.
|
|
84
|
+
"""
|
|
85
|
+
# Lazy import to avoid circular imports
|
|
86
|
+
global HealthCheckFilter
|
|
87
|
+
if HealthCheckFilter is None:
|
|
88
|
+
from .logging_filters import HealthCheckFilter as _HealthCheckFilter
|
|
89
|
+
HealthCheckFilter = _HealthCheckFilter
|
|
90
|
+
|
|
91
|
+
# Get log level from settings, default to INFO if not found
|
|
92
|
+
try:
|
|
93
|
+
log_level = settings.logging.level
|
|
94
|
+
except AttributeError:
|
|
95
|
+
log_level = 'INFO'
|
|
96
|
+
numeric_level = logging.getLevelName(log_level.upper())
|
|
97
|
+
|
|
98
|
+
# Configure health check filter for uvicorn access logs
|
|
99
|
+
health_check_filters = []
|
|
100
|
+
health_check_path = getattr(settings, 'excluded_urls', '')
|
|
101
|
+
if health_check_path and HealthCheckFilter is not None:
|
|
102
|
+
health_check_filters.append({
|
|
103
|
+
'class': HealthCheckFilter,
|
|
104
|
+
'path': health_check_path
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
# 1. Define a standard Python Formatter
|
|
108
|
+
service_name = getattr(getattr(settings, 'service', None), 'name', 'guardianhub')
|
|
109
|
+
standard_formatter = logging.Formatter(
|
|
110
|
+
# Format: [2025-10-12 00:50:20] INFO: service_name.module_name - message
|
|
111
|
+
fmt=f"[%(asctime)s] %(levelname)s: {service_name}.%(name)s - %(message)s",
|
|
112
|
+
datefmt="%Y-%m-%d %H:%M:%S"
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# 2. Setup the Root Logger (for ALL application logs, including lifespan)
|
|
116
|
+
root_logger = logging.getLogger()
|
|
117
|
+
root_logger.setLevel(numeric_level)
|
|
118
|
+
|
|
119
|
+
# Clear existing handlers (critical for clean reconfiguration)
|
|
120
|
+
for handler in root_logger.handlers[:]:
|
|
121
|
+
root_logger.removeHandler(handler)
|
|
122
|
+
|
|
123
|
+
# Add the main application handler
|
|
124
|
+
app_handler = logging.StreamHandler(sys.stdout)
|
|
125
|
+
app_handler.setFormatter(standard_formatter)
|
|
126
|
+
app_handler.setLevel(numeric_level)
|
|
127
|
+
root_logger.addHandler(app_handler)
|
|
128
|
+
|
|
129
|
+
# 3. Setup the Uvicorn Access Logger (for HTTP request logs)
|
|
130
|
+
uvicorn_access_logger = logging.getLogger("uvicorn.access")
|
|
131
|
+
uvicorn_access_logger.setLevel(numeric_level)
|
|
132
|
+
# This prevents uvicorn logs from being duplicated by propagating to the root_logger
|
|
133
|
+
uvicorn_access_logger.propagate = False
|
|
134
|
+
|
|
135
|
+
# Clear existing uvicorn handlers
|
|
136
|
+
for handler in uvicorn_access_logger.handlers[:]:
|
|
137
|
+
uvicorn_access_logger.removeHandler(handler)
|
|
138
|
+
|
|
139
|
+
# Apply health check filters
|
|
140
|
+
for f in health_check_filters:
|
|
141
|
+
if isinstance(f, dict):
|
|
142
|
+
filter_class = f.pop('class')
|
|
143
|
+
uvicorn_access_logger.addFilter(filter_class(**f))
|
|
144
|
+
else:
|
|
145
|
+
uvicorn_access_logger.addFilter(f)
|
|
146
|
+
|
|
147
|
+
# Add the dedicated uvicorn handler
|
|
148
|
+
uvicorn_handler = logging.StreamHandler(sys.stdout)
|
|
149
|
+
uvicorn_handler.setFormatter(standard_formatter)
|
|
150
|
+
|
|
151
|
+
# Assuming HealthCheckFilter is defined and available
|
|
152
|
+
try:
|
|
153
|
+
from .logging_filters import HealthCheckFilter
|
|
154
|
+
uvicorn_handler.addFilter(HealthCheckFilter(path="/health"))
|
|
155
|
+
except ImportError:
|
|
156
|
+
# Fallback if the filter cannot be imported
|
|
157
|
+
pass
|
|
158
|
+
|
|
159
|
+
uvicorn_handler.setLevel(numeric_level)
|
|
160
|
+
uvicorn_access_logger.addHandler(uvicorn_handler)
|
|
161
|
+
|
|
162
|
+
# Get service name safely with fallback
|
|
163
|
+
service_name = getattr(getattr(settings, 'service', None), 'name', 'guardianhub')
|
|
164
|
+
|
|
165
|
+
# Final log message
|
|
166
|
+
root_logger.info(
|
|
167
|
+
f"Logging for '{service_name}' configured at level {logging.getLevelName(numeric_level)} with standard format."
|
|
168
|
+
)
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Custom logging filters to reduce log noise."""
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
class HealthCheckFilter(logging.Filter):
|
|
5
|
+
"""A custom log filter to suppress successful health check endpoint logs."""
|
|
6
|
+
|
|
7
|
+
def __init__(self, path: str):
|
|
8
|
+
"""Initialize the filter with the path of the health check endpoint."""
|
|
9
|
+
super().__init__()
|
|
10
|
+
self.path = path
|
|
11
|
+
|
|
12
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
13
|
+
"""Filter out successful log records for the health check endpoint.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
record: The log record to be processed.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
False if the record should be suppressed, True otherwise.
|
|
20
|
+
"""
|
|
21
|
+
# --- CORRECTED FILTER LOGIC ---
|
|
22
|
+
# The args tuple from uvicorn.access is:
|
|
23
|
+
# (client_addr, method, path, protocol, status_code)
|
|
24
|
+
# We need to check if the tuple has enough elements to avoid an IndexError.
|
|
25
|
+
if record.name == "uvicorn.access" and isinstance(record.args, tuple) and len(record.args) >= 5:
|
|
26
|
+
# Extract the path (at index 2) and status_code (at index 4)
|
|
27
|
+
path = record.args[2]
|
|
28
|
+
status_code = record.args[4]
|
|
29
|
+
|
|
30
|
+
# Check for the specific health check path and a 200 status code
|
|
31
|
+
if path == self.path and status_code == 200:
|
|
32
|
+
return False
|
|
33
|
+
|
|
34
|
+
# Allow all other log records to pass
|
|
35
|
+
return True
|
|
File without changes
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# models/agent_models.py
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
from enum import Enum
|
|
4
|
+
from typing import Dict, List, Optional, Any
|
|
5
|
+
from pydantic import BaseModel, Field
|
|
6
|
+
from .registry.registry import register_model
|
|
7
|
+
|
|
8
|
+
@register_model
|
|
9
|
+
class AgentCreateRequest(BaseModel):
|
|
10
|
+
"""Request model for creating a new agent with flexible configuration.
|
|
11
|
+
|
|
12
|
+
Example:
|
|
13
|
+
```json
|
|
14
|
+
{
|
|
15
|
+
"agent_name": "social-media-ai",
|
|
16
|
+
"agent_type": "social_media",
|
|
17
|
+
"description": "AI assistant for managing social media presence",
|
|
18
|
+
"system_prompt": "You are a helpful social media assistant...",
|
|
19
|
+
"config": {
|
|
20
|
+
"target_platforms": ["LinkedIn", "Twitter", "Instagram"],
|
|
21
|
+
"posting_schedule": {
|
|
22
|
+
"Monday": ["10:00", "15:00"],
|
|
23
|
+
"Wednesday": ["11:00", "17:00"]
|
|
24
|
+
},
|
|
25
|
+
"content_themes": ["Industry news", "Company updates"]
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
"""
|
|
29
|
+
agent_name: str = Field(
|
|
30
|
+
example="social-media-ai",
|
|
31
|
+
description="Unique identifier for the agent (e.g., 'social-media-ai', 'customer-support-bot')."
|
|
32
|
+
)
|
|
33
|
+
agent_type: str = Field(
|
|
34
|
+
example="social_media",
|
|
35
|
+
description="Type of the agent (e.g., 'social_media', 'customer_support', 'data_analyst')."
|
|
36
|
+
)
|
|
37
|
+
description: str = Field(
|
|
38
|
+
default="",
|
|
39
|
+
example="AI assistant for managing social media presence",
|
|
40
|
+
description="Brief description of the agent's purpose and functionality."
|
|
41
|
+
)
|
|
42
|
+
system_prompt: str = Field(
|
|
43
|
+
example="You are a helpful social media assistant that creates engaging content...",
|
|
44
|
+
description="The system prompt that defines the agent's behavior and capabilities."
|
|
45
|
+
)
|
|
46
|
+
config: Dict[str, Any] = Field(
|
|
47
|
+
default_factory=dict,
|
|
48
|
+
example={
|
|
49
|
+
"target_platforms": ["LinkedIn", "Twitter", "Instagram"],
|
|
50
|
+
"posting_schedule": {
|
|
51
|
+
"Monday": ["10:00", "15:00"],
|
|
52
|
+
"Wednesday": ["11:00"]
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
description="Agent-specific configuration parameters as key-value pairs."
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
class AgentStatus(str, Enum):
|
|
59
|
+
DRAFT = "DRAFT"
|
|
60
|
+
ACTIVE = "ACTIVE"
|
|
61
|
+
INACTIVE = "INACTIVE"
|
|
62
|
+
TRAINING = "TRAINING"
|
|
63
|
+
|
|
64
|
+
@register_model
|
|
65
|
+
class AgentCreate(BaseModel):
|
|
66
|
+
name: str = Field(..., description="Name of the agent")
|
|
67
|
+
domain: str = Field(..., description="Domain of the agent")
|
|
68
|
+
description: Optional[str] = Field(None, description="Description of the agent")
|
|
69
|
+
system_prompt: str = Field(..., description="System prompt for the agent")
|
|
70
|
+
status: AgentStatus = Field(AgentStatus.DRAFT, description="Status of the agent")
|
|
71
|
+
tags: List[str] = Field(default_factory=list, description="Tags for the agent")
|
|
72
|
+
default_tools: List[str] = Field(default_factory=list, description="List of default tool IDs")
|
|
73
|
+
reflection_config: Dict[str, Any] = Field(
|
|
74
|
+
default_factory=dict,
|
|
75
|
+
description="Configuration for agent reflection and learning"
|
|
76
|
+
)
|
|
77
|
+
metadata: Dict[str, Any] = Field(
|
|
78
|
+
default_factory=dict,
|
|
79
|
+
description="Additional metadata for the agent"
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
@register_model
|
|
83
|
+
class Agent(AgentCreate):
|
|
84
|
+
id: str = Field(..., description="Unique identifier for the agent")
|
|
85
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
86
|
+
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
|
87
|
+
|
|
88
|
+
class Config:
|
|
89
|
+
from_attributes = True
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@register_model
|
|
93
|
+
class AgentResponse(BaseModel):
|
|
94
|
+
"""Response model for agent operations."""
|
|
95
|
+
id: str = Field(..., description="Unique identifier for the agent")
|
|
96
|
+
name: str = Field(..., description="Name of the agent")
|
|
97
|
+
agent_type: str = Field(..., description="Type of the agent")
|
|
98
|
+
status: AgentStatus = Field(..., description="Current status of the agent")
|
|
99
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
100
|
+
updated_at: datetime = Field(default_factory=datetime.utcnow)
|
|
101
|
+
system_prompt: str = Field(..., description="System prompt for the agent")
|
|
102
|
+
domain: str = Field(..., description="Domain of the agent")
|
|
103
|
+
description: str = Field(default="", description="Description of the agent")
|
|
104
|
+
message: Optional[str] = Field(None, description="Additional details about the operation")
|
|
105
|
+
config: Dict[str, Any] = Field(
|
|
106
|
+
default_factory=dict,
|
|
107
|
+
description="Agent-specific configuration parameters as key-value pairs."
|
|
108
|
+
)
|
|
109
|
+
class Config:
|
|
110
|
+
from_attributes = True
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# Wrapper model to ensure the LLM outputs a single JSON object.
|
|
114
|
+
@register_model
|
|
115
|
+
class SearchPhrasesResponse(BaseModel):
|
|
116
|
+
"""The expected structure for the LLM output."""
|
|
117
|
+
search_phrases: List[str] = Field(
|
|
118
|
+
...,
|
|
119
|
+
description="A list of 1 to 3 search phrases relevant to the agent's domain."
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
@register_model
|
|
123
|
+
class LLMReflectionConfig(BaseModel):
|
|
124
|
+
"""Domain-specific reflection parameters to override defaults."""
|
|
125
|
+
enabled: Optional[bool] = Field(None, description="Whether ACE reflection should be enabled.")
|
|
126
|
+
optimization_types: List[str] = Field(default_factory=list,
|
|
127
|
+
description="List of domain-specific optimization types (e.g., DataQuality, Sentiment).")
|
|
128
|
+
synthesis_directives: Optional[Dict[str, Any]] = Field(None,
|
|
129
|
+
description="Directives for semantic tool synthesis (e.g., concrete_tool_whitelist).")
|
|
130
|
+
@register_model
|
|
131
|
+
class LLMKnowledgeSuggestion(BaseModel):
|
|
132
|
+
"""Suggestions for initial knowledge base articles or queries."""
|
|
133
|
+
type: str = Field(..., description="Type of knowledge (e.g., 'document', 'query').")
|
|
134
|
+
value: str = Field(..., description="The document ID, query text, or relevant URL.")
|
|
135
|
+
|
|
136
|
+
@register_model
|
|
137
|
+
class AgentLLMConfigSuggestion(BaseModel):
|
|
138
|
+
"""
|
|
139
|
+
Structured configuration output generated by the LLM for a new agent.
|
|
140
|
+
This replaces the unreliable Dict[str, Any] response.
|
|
141
|
+
"""
|
|
142
|
+
system_prompt: str = Field(...,
|
|
143
|
+
description="The comprehensive, domain-specific system prompt and persona for the agent's core LLM.")
|
|
144
|
+
|
|
145
|
+
# Use the structured reflection config model
|
|
146
|
+
reflection_config: LLMReflectionConfig = Field(...,
|
|
147
|
+
description="Structured parameters for the agent's ACE reflection process.")
|
|
148
|
+
|
|
149
|
+
# Suggestions for the warm-up phase (Initial query generation and knowledge seeding)
|
|
150
|
+
initial_warmup_query: str = Field(...,
|
|
151
|
+
description="The most effective synthetic query to run for the initial ACE warm-up.")
|
|
152
|
+
knowledge_suggestions: List[LLMKnowledgeSuggestion] = Field(default_factory=list,
|
|
153
|
+
description="Suggestions for initial knowledge to pull or seed.")
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# guardianhub_sdk/models/registry/client.py
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from ...models.registry.loader import Loader
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ModelRegistryClient:
|
|
7
|
+
def __init__(self, base_url: str, token: Optional[str] = None):
|
|
8
|
+
self.base_url = base_url.rstrip('/')
|
|
9
|
+
self.loader = Loader()
|
|
10
|
+
|
|
11
|
+
async def fetch_model(self, name: str, version: Optional[str] = None):
|
|
12
|
+
"""Fetch remote model metadata and python artifact, return a loaded class or metadata."""
|
|
13
|
+
return await self.loader.load(name)
|
|
14
|
+
|
|
15
|
+
# singleton-ish convenience
|
|
16
|
+
client = ModelRegistryClient(base_url="https://registry.internal.local")
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
from typing import Dict, Any, Type, Optional, List
|
|
2
|
+
from pydantic import BaseModel, create_model
|
|
3
|
+
from pydantic.fields import Field
|
|
4
|
+
|
|
5
|
+
_DYNAMIC_MODEL_CACHE: Dict[str, Type[BaseModel]] = {}
|
|
6
|
+
|
|
7
|
+
class DynamicModelBuilder:
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def load_from_schema(
|
|
11
|
+
model_name: str,
|
|
12
|
+
schema_json: Dict[str, Any]
|
|
13
|
+
) -> Type[BaseModel]:
|
|
14
|
+
|
|
15
|
+
# 1: Cache
|
|
16
|
+
if model_name in _DYNAMIC_MODEL_CACHE:
|
|
17
|
+
return _DYNAMIC_MODEL_CACHE[model_name]
|
|
18
|
+
|
|
19
|
+
# 2: Generate
|
|
20
|
+
model = DynamicModelBuilder._create_model(model_name, schema_json)
|
|
21
|
+
_DYNAMIC_MODEL_CACHE[model_name] = model
|
|
22
|
+
return model
|
|
23
|
+
|
|
24
|
+
@staticmethod
|
|
25
|
+
def _create_model(
|
|
26
|
+
model_name: str,
|
|
27
|
+
schema: Dict[str, Any]
|
|
28
|
+
) -> Type[BaseModel]:
|
|
29
|
+
|
|
30
|
+
properties = schema.get("properties", {})
|
|
31
|
+
required_fields = schema.get("required", [])
|
|
32
|
+
|
|
33
|
+
model_fields = {}
|
|
34
|
+
|
|
35
|
+
for field_name, field_schema in properties.items():
|
|
36
|
+
field_type = DynamicModelBuilder._map_schema_to_type(field_schema)
|
|
37
|
+
|
|
38
|
+
default_value = ...
|
|
39
|
+
if field_name not in required_fields:
|
|
40
|
+
default_value = None
|
|
41
|
+
|
|
42
|
+
model_fields[field_name] = (field_type, default_value)
|
|
43
|
+
|
|
44
|
+
return create_model(model_name, **model_fields)
|
|
45
|
+
|
|
46
|
+
@staticmethod
|
|
47
|
+
def _map_schema_to_type(schema: Dict[str, Any]):
|
|
48
|
+
"""Map JSON Schema types to Python."""
|
|
49
|
+
t = schema.get("type")
|
|
50
|
+
|
|
51
|
+
# Primitive
|
|
52
|
+
if t == "string":
|
|
53
|
+
return str
|
|
54
|
+
if t == "number":
|
|
55
|
+
return float
|
|
56
|
+
if t == "integer":
|
|
57
|
+
return int
|
|
58
|
+
if t == "boolean":
|
|
59
|
+
return bool
|
|
60
|
+
|
|
61
|
+
# Array
|
|
62
|
+
if t == "array":
|
|
63
|
+
item_schema = schema.get("items", {})
|
|
64
|
+
item_type = DynamicModelBuilder._map_schema_to_type(item_schema)
|
|
65
|
+
return List[item_type]
|
|
66
|
+
|
|
67
|
+
# Object
|
|
68
|
+
if t == "object":
|
|
69
|
+
inner_model_name = "NestedObject"
|
|
70
|
+
return DynamicModelBuilder._create_model(inner_model_name, schema)
|
|
71
|
+
|
|
72
|
+
# Fallback
|
|
73
|
+
return Any
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Type, Dict, Any, Optional
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
from .registry import MODEL_REGISTRY, get_model
|
|
4
|
+
from .dynamic_loader import DynamicModelBuilder
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Loader:
|
|
8
|
+
|
|
9
|
+
@staticmethod
|
|
10
|
+
def load(
|
|
11
|
+
model_name: Optional[str] = None,
|
|
12
|
+
schema_json: Optional[Dict[str, Any]] = None
|
|
13
|
+
) -> Type[BaseModel]:
|
|
14
|
+
"""
|
|
15
|
+
Load a Pydantic model either by registry name or dynamically from schema.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
model_name: Optional name of a registered model
|
|
19
|
+
schema_json: Optional JSON schema dict to build a dynamic model
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
Pydantic model class
|
|
23
|
+
"""
|
|
24
|
+
# 1. Try registry first
|
|
25
|
+
if model_name:
|
|
26
|
+
try:
|
|
27
|
+
return get_model(model_name)
|
|
28
|
+
except ValueError:
|
|
29
|
+
pass # fallback to schema if provided
|
|
30
|
+
|
|
31
|
+
# 2. Generate dynamically from schema
|
|
32
|
+
if schema_json:
|
|
33
|
+
dynamic_name = model_name or "DynamicModel"
|
|
34
|
+
return DynamicModelBuilder.load_from_schema(dynamic_name, schema_json)
|
|
35
|
+
|
|
36
|
+
# 3. Neither provided → raise error
|
|
37
|
+
raise ValueError("Either a registered model name or schema JSON must be provided")
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Dict, Type
|
|
2
|
+
from pydantic import BaseModel
|
|
3
|
+
|
|
4
|
+
MODEL_REGISTRY: Dict[str, Type[BaseModel]] = {}
|
|
5
|
+
|
|
6
|
+
def register_model(model: Type[BaseModel]):
|
|
7
|
+
"""
|
|
8
|
+
Register a Pydantic model so it can be dynamically loaded across microservices.
|
|
9
|
+
"""
|
|
10
|
+
MODEL_REGISTRY[model.__name__] = model
|
|
11
|
+
return model
|
|
12
|
+
|
|
13
|
+
def get_model(name: str) -> Type[BaseModel]:
|
|
14
|
+
try:
|
|
15
|
+
return MODEL_REGISTRY[name]
|
|
16
|
+
except KeyError:
|
|
17
|
+
raise ValueError(f"Model '{name}' not found in registry")
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# guardianhub_sdk/models/registry/signing.py
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Tuple, Dict, Any
|
|
4
|
+
import base64
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from Crypto.PublicKey import RSA
|
|
8
|
+
from Crypto.Signature import pkcs1_15
|
|
9
|
+
from Crypto.Hash import SHA256
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def generate_rsa_keypair(bits: int = 4096) -> Tuple[bytes, bytes]:
|
|
13
|
+
"""
|
|
14
|
+
Generate RSA keypair (private_pem, public_pem).
|
|
15
|
+
In production use: KMS or offline secure key generation.
|
|
16
|
+
"""
|
|
17
|
+
key = RSA.generate(bits)
|
|
18
|
+
private_pem = key.export_key(format="PEM")
|
|
19
|
+
public_pem = key.publickey().export_key(format="PEM")
|
|
20
|
+
return private_pem, public_pem
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def sign_bytes(private_pem: bytes, payload: bytes) -> str:
|
|
24
|
+
"""
|
|
25
|
+
Sign arbitrary bytes with RSA-SHA256, return base64 signature string.
|
|
26
|
+
"""
|
|
27
|
+
key = RSA.import_key(private_pem)
|
|
28
|
+
h = SHA256.new(payload)
|
|
29
|
+
signature = pkcs1_15.new(key).sign(h)
|
|
30
|
+
return base64.b64encode(signature).decode("ascii")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def verify_bytes(public_pem: bytes, payload: bytes, signature_b64: str) -> bool:
|
|
34
|
+
"""
|
|
35
|
+
Verify a base64 signature over payload bytes using public key.
|
|
36
|
+
"""
|
|
37
|
+
key = RSA.import_key(public_pem)
|
|
38
|
+
h = SHA256.new(payload)
|
|
39
|
+
signature = base64.b64decode(signature_b64.encode("ascii"))
|
|
40
|
+
try:
|
|
41
|
+
pkcs1_15.new(key).verify(h, signature)
|
|
42
|
+
return True
|
|
43
|
+
except (ValueError, TypeError):
|
|
44
|
+
return False
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def sign_metadata_dict(private_pem: bytes, metadata: Dict[str, Any]) -> Dict[str, Any]:
|
|
48
|
+
"""
|
|
49
|
+
Attach a signature to the metadata dict and return augmented metadata.
|
|
50
|
+
Uses canonical JSON encoding (sorted keys, no whitespace).
|
|
51
|
+
"""
|
|
52
|
+
payload = json.dumps(metadata, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
|
53
|
+
sig = sign_bytes(private_pem, payload)
|
|
54
|
+
metadata_signed = dict(metadata)
|
|
55
|
+
metadata_signed["_signature"] = sig
|
|
56
|
+
return metadata_signed
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def verify_metadata_dict(public_pem: bytes, metadata_signed: Dict[str, Any]) -> bool:
|
|
60
|
+
"""
|
|
61
|
+
Verify `_signature` in metadata_signed.
|
|
62
|
+
Returns True if valid, False otherwise.
|
|
63
|
+
"""
|
|
64
|
+
sig = metadata_signed.get("_signature")
|
|
65
|
+
if not sig:
|
|
66
|
+
return False
|
|
67
|
+
md = dict(metadata_signed)
|
|
68
|
+
md.pop("_signature", None)
|
|
69
|
+
payload = json.dumps(md, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
|
70
|
+
return verify_bytes(public_pem, payload, sig)
|
|
File without changes
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
from typing import List, Dict, Any, Optional, Literal
|
|
2
|
+
from pydantic import BaseModel, Field, conlist
|
|
3
|
+
from ..registry.registry import register_model
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# --- STEP 1: Define the individual tool-call step model ---
|
|
7
|
+
class PlanStep(BaseModel):
|
|
8
|
+
"""A single step in the agent's overall macro-plan."""
|
|
9
|
+
|
|
10
|
+
# The exact name of the tool to be called (must match a registered tool)
|
|
11
|
+
tool_name: str = Field(..., description="The exact name of the tool to execute.")
|
|
12
|
+
|
|
13
|
+
# A unique name for this step, used as the key to store the result in macro_context.
|
|
14
|
+
step_name: str = Field(...,
|
|
15
|
+
description="A unique identifier for this plan step. Its result will be stored under this name in the macro_context.")
|
|
16
|
+
|
|
17
|
+
# The arguments for the tool call, passed as a dictionary.
|
|
18
|
+
tool_args: Dict[str, Any] = Field(..., description="The dictionary of arguments required for the tool call.")
|
|
19
|
+
|
|
20
|
+
# The list of results (by step_name) this step requires before it can be executed.
|
|
21
|
+
dependencies: List[str] = Field(
|
|
22
|
+
default_factory=list,
|
|
23
|
+
description="List of step_name strings whose results must be available in macro_context before this step can run."
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
# Internal status, handled by the graph executor.
|
|
27
|
+
status: Literal["pending", "running", "complete", "error"] = Field(
|
|
28
|
+
default="pending",
|
|
29
|
+
description="The current status of the step (pending, running, complete, error). Must be 'pending' initially."
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# --- STEP 2: Define the Macro Plan model ---
|
|
34
|
+
@register_model
|
|
35
|
+
class MacroPlan(BaseModel):
|
|
36
|
+
"""The complete structured plan generated by the LLM."""
|
|
37
|
+
|
|
38
|
+
# Must be an array of PlanStep objects.
|
|
39
|
+
plan: conlist(PlanStep, min_length=0) = Field(
|
|
40
|
+
default_factory=list, # Use default_factory for mutable types
|
|
41
|
+
description="The sequence of PlanStep objects required to fulfill the user request. Can be empty if no tools are needed."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
# A final reflection or thought process before synthesis/execution.
|
|
45
|
+
# 🟢 FIX: Make 'reflection' Optional and provide an empty string default.
|
|
46
|
+
# This allows the model to validate even if the key is missing from the LLM's JSON.
|
|
47
|
+
reflection: Optional[str] = Field(
|
|
48
|
+
default="",
|
|
49
|
+
description="A brief thought process explaining why this plan was chosen and how it addresses the user's input."
|
|
50
|
+
)
|
|
51
|
+
metadata: Dict[str, Any] = Field(
|
|
52
|
+
...,
|
|
53
|
+
description=(
|
|
54
|
+
"Additional metadata about the plan in a Pydantic-compatible JSON Schema format. "
|
|
55
|
+
"This can include configuration parameters, execution context, or any other "
|
|
56
|
+
"plan-specific metadata needed for execution or analysis."
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class MacroPlanResponse(BaseModel):
|
|
63
|
+
"""Response model for the LLM's planning output."""
|
|
64
|
+
plan: List[PlanStep] = Field(default_factory=list, description="List of plan steps")
|
|
65
|
+
reflection: Optional[str] = Field(default="", description="Reasoning behind the plan")
|