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.
Files changed (64) hide show
  1. guardianhub/__init__.py +29 -0
  2. guardianhub/_version.py +1 -0
  3. guardianhub/agents/runtime.py +12 -0
  4. guardianhub/auth/token_provider.py +22 -0
  5. guardianhub/clients/__init__.py +2 -0
  6. guardianhub/clients/classification_client.py +52 -0
  7. guardianhub/clients/graph_db_client.py +161 -0
  8. guardianhub/clients/langfuse/dataset_client.py +157 -0
  9. guardianhub/clients/langfuse/manager.py +118 -0
  10. guardianhub/clients/langfuse/prompt_client.py +68 -0
  11. guardianhub/clients/langfuse/score_evaluation_client.py +92 -0
  12. guardianhub/clients/langfuse/tracing_client.py +250 -0
  13. guardianhub/clients/langfuse_client.py +63 -0
  14. guardianhub/clients/llm_client.py +144 -0
  15. guardianhub/clients/llm_service.py +295 -0
  16. guardianhub/clients/metadata_extractor_client.py +53 -0
  17. guardianhub/clients/ocr_client.py +81 -0
  18. guardianhub/clients/paperless_client.py +515 -0
  19. guardianhub/clients/registry_client.py +18 -0
  20. guardianhub/clients/text_cleaner_client.py +58 -0
  21. guardianhub/clients/vector_client.py +344 -0
  22. guardianhub/config/__init__.py +0 -0
  23. guardianhub/config/config_development.json +84 -0
  24. guardianhub/config/config_prod.json +39 -0
  25. guardianhub/config/settings.py +221 -0
  26. guardianhub/http/http_client.py +26 -0
  27. guardianhub/logging/__init__.py +2 -0
  28. guardianhub/logging/logging.py +168 -0
  29. guardianhub/logging/logging_filters.py +35 -0
  30. guardianhub/models/__init__.py +0 -0
  31. guardianhub/models/agent_models.py +153 -0
  32. guardianhub/models/base.py +2 -0
  33. guardianhub/models/registry/client.py +16 -0
  34. guardianhub/models/registry/dynamic_loader.py +73 -0
  35. guardianhub/models/registry/loader.py +37 -0
  36. guardianhub/models/registry/registry.py +17 -0
  37. guardianhub/models/registry/signing.py +70 -0
  38. guardianhub/models/template/__init__.py +0 -0
  39. guardianhub/models/template/agent_plan.py +65 -0
  40. guardianhub/models/template/agent_response_evaluation.py +67 -0
  41. guardianhub/models/template/extraction.py +29 -0
  42. guardianhub/models/template/reflection_critique.py +206 -0
  43. guardianhub/models/template/suggestion.py +42 -0
  44. guardianhub/observability/__init__.py +1 -0
  45. guardianhub/observability/instrumentation.py +271 -0
  46. guardianhub/observability/otel_helper.py +43 -0
  47. guardianhub/observability/otel_middlewares.py +73 -0
  48. guardianhub/prompts/base.py +7 -0
  49. guardianhub/prompts/providers/langfuse_provider.py +13 -0
  50. guardianhub/prompts/providers/local_provider.py +22 -0
  51. guardianhub/prompts/registry.py +14 -0
  52. guardianhub/scripts/script.sh +31 -0
  53. guardianhub/services/base.py +15 -0
  54. guardianhub/template/__init__.py +0 -0
  55. guardianhub/tools/gh_registry_cli.py +171 -0
  56. guardianhub/utils/__init__.py +0 -0
  57. guardianhub/utils/app_state.py +74 -0
  58. guardianhub/utils/fastapi_utils.py +152 -0
  59. guardianhub/utils/json_utils.py +137 -0
  60. guardianhub/utils/metrics.py +60 -0
  61. guardianhub-0.1.88.dist-info/METADATA +240 -0
  62. guardianhub-0.1.88.dist-info/RECORD +64 -0
  63. guardianhub-0.1.88.dist-info/WHEEL +4 -0
  64. 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,2 @@
1
+ from .logging import setup_logging,get_logger
2
+ __all__ = ["setup_logging","get_logger"]
@@ -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,2 @@
1
+ class BaseModel:
2
+ pass
@@ -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")