proxilion 0.0.1__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 (94) hide show
  1. proxilion/__init__.py +136 -0
  2. proxilion/audit/__init__.py +133 -0
  3. proxilion/audit/base_exporters.py +527 -0
  4. proxilion/audit/compliance/__init__.py +130 -0
  5. proxilion/audit/compliance/base.py +457 -0
  6. proxilion/audit/compliance/eu_ai_act.py +603 -0
  7. proxilion/audit/compliance/iso27001.py +544 -0
  8. proxilion/audit/compliance/soc2.py +491 -0
  9. proxilion/audit/events.py +493 -0
  10. proxilion/audit/explainability.py +1173 -0
  11. proxilion/audit/exporters/__init__.py +58 -0
  12. proxilion/audit/exporters/aws_s3.py +636 -0
  13. proxilion/audit/exporters/azure_storage.py +608 -0
  14. proxilion/audit/exporters/cloud_base.py +468 -0
  15. proxilion/audit/exporters/gcp_storage.py +570 -0
  16. proxilion/audit/exporters/multi_exporter.py +498 -0
  17. proxilion/audit/hash_chain.py +652 -0
  18. proxilion/audit/logger.py +543 -0
  19. proxilion/caching/__init__.py +49 -0
  20. proxilion/caching/tool_cache.py +633 -0
  21. proxilion/context/__init__.py +73 -0
  22. proxilion/context/context_window.py +556 -0
  23. proxilion/context/message_history.py +505 -0
  24. proxilion/context/session.py +735 -0
  25. proxilion/contrib/__init__.py +51 -0
  26. proxilion/contrib/anthropic.py +609 -0
  27. proxilion/contrib/google.py +1012 -0
  28. proxilion/contrib/langchain.py +641 -0
  29. proxilion/contrib/mcp.py +893 -0
  30. proxilion/contrib/openai.py +646 -0
  31. proxilion/core.py +3058 -0
  32. proxilion/decorators.py +966 -0
  33. proxilion/engines/__init__.py +287 -0
  34. proxilion/engines/base.py +266 -0
  35. proxilion/engines/casbin_engine.py +412 -0
  36. proxilion/engines/opa_engine.py +493 -0
  37. proxilion/engines/simple.py +437 -0
  38. proxilion/exceptions.py +887 -0
  39. proxilion/guards/__init__.py +54 -0
  40. proxilion/guards/input_guard.py +522 -0
  41. proxilion/guards/output_guard.py +634 -0
  42. proxilion/observability/__init__.py +198 -0
  43. proxilion/observability/cost_tracker.py +866 -0
  44. proxilion/observability/hooks.py +683 -0
  45. proxilion/observability/metrics.py +798 -0
  46. proxilion/observability/session_cost_tracker.py +1063 -0
  47. proxilion/policies/__init__.py +67 -0
  48. proxilion/policies/base.py +304 -0
  49. proxilion/policies/builtin.py +486 -0
  50. proxilion/policies/registry.py +376 -0
  51. proxilion/providers/__init__.py +201 -0
  52. proxilion/providers/adapter.py +468 -0
  53. proxilion/providers/anthropic_adapter.py +330 -0
  54. proxilion/providers/gemini_adapter.py +391 -0
  55. proxilion/providers/openai_adapter.py +294 -0
  56. proxilion/py.typed +0 -0
  57. proxilion/resilience/__init__.py +81 -0
  58. proxilion/resilience/degradation.py +615 -0
  59. proxilion/resilience/fallback.py +555 -0
  60. proxilion/resilience/retry.py +554 -0
  61. proxilion/scheduling/__init__.py +57 -0
  62. proxilion/scheduling/priority_queue.py +419 -0
  63. proxilion/scheduling/scheduler.py +459 -0
  64. proxilion/security/__init__.py +244 -0
  65. proxilion/security/agent_trust.py +968 -0
  66. proxilion/security/behavioral_drift.py +794 -0
  67. proxilion/security/cascade_protection.py +869 -0
  68. proxilion/security/circuit_breaker.py +428 -0
  69. proxilion/security/cost_limiter.py +690 -0
  70. proxilion/security/idor_protection.py +460 -0
  71. proxilion/security/intent_capsule.py +849 -0
  72. proxilion/security/intent_validator.py +495 -0
  73. proxilion/security/memory_integrity.py +767 -0
  74. proxilion/security/rate_limiter.py +509 -0
  75. proxilion/security/scope_enforcer.py +680 -0
  76. proxilion/security/sequence_validator.py +636 -0
  77. proxilion/security/trust_boundaries.py +784 -0
  78. proxilion/streaming/__init__.py +70 -0
  79. proxilion/streaming/detector.py +761 -0
  80. proxilion/streaming/transformer.py +674 -0
  81. proxilion/timeouts/__init__.py +55 -0
  82. proxilion/timeouts/decorators.py +477 -0
  83. proxilion/timeouts/manager.py +545 -0
  84. proxilion/tools/__init__.py +69 -0
  85. proxilion/tools/decorators.py +493 -0
  86. proxilion/tools/registry.py +732 -0
  87. proxilion/types.py +339 -0
  88. proxilion/validation/__init__.py +93 -0
  89. proxilion/validation/pydantic_schema.py +351 -0
  90. proxilion/validation/schema.py +651 -0
  91. proxilion-0.0.1.dist-info/METADATA +872 -0
  92. proxilion-0.0.1.dist-info/RECORD +94 -0
  93. proxilion-0.0.1.dist-info/WHEEL +4 -0
  94. proxilion-0.0.1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,493 @@
1
+ """
2
+ Decorators for tool registration.
3
+
4
+ Provides convenient decorators for registering functions as tools
5
+ with automatic schema inference from type hints.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import functools
11
+ import inspect
12
+ import logging
13
+ from collections.abc import Callable
14
+ from typing import Any, TypeVar, Union, get_args, get_origin, get_type_hints
15
+
16
+ from proxilion.tools.registry import (
17
+ RiskLevel,
18
+ ToolCategory,
19
+ ToolDefinition,
20
+ ToolRegistry,
21
+ get_global_registry,
22
+ )
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+ F = TypeVar("F", bound=Callable[..., Any])
27
+
28
+
29
+ def infer_schema_from_function(func: Callable[..., Any]) -> dict[str, Any]:
30
+ """
31
+ Infer a JSON Schema from function signature and type hints.
32
+
33
+ Analyzes the function's parameters and type annotations to generate
34
+ a JSON Schema compatible with OpenAI/Anthropic tool definitions.
35
+
36
+ Args:
37
+ func: The function to analyze.
38
+
39
+ Returns:
40
+ A JSON Schema dictionary describing the function's parameters.
41
+
42
+ Example:
43
+ >>> def search(query: str, max_results: int = 10) -> list[dict]:
44
+ ... pass
45
+ >>> schema = infer_schema_from_function(search)
46
+ >>> schema["type"]
47
+ 'object'
48
+ >>> "query" in schema["properties"]
49
+ True
50
+ """
51
+ schema: dict[str, Any] = {
52
+ "type": "object",
53
+ "properties": {},
54
+ "required": [],
55
+ }
56
+
57
+ sig = inspect.signature(func)
58
+
59
+ # Try to get type hints, handling potential errors
60
+ try:
61
+ hints = get_type_hints(func)
62
+ except Exception:
63
+ hints = {}
64
+
65
+ for param_name, param in sig.parameters.items():
66
+ # Skip self, cls, *args, **kwargs
67
+ if param_name in ("self", "cls"):
68
+ continue
69
+ if param.kind in (
70
+ inspect.Parameter.VAR_POSITIONAL,
71
+ inspect.Parameter.VAR_KEYWORD,
72
+ ):
73
+ continue
74
+
75
+ # Get type annotation
76
+ type_hint = hints.get(param_name, param.annotation)
77
+ if type_hint is inspect.Parameter.empty:
78
+ type_hint = Any
79
+
80
+ # Convert type to JSON Schema
81
+ prop_schema = _type_to_schema(type_hint)
82
+
83
+ # Add description from docstring if available
84
+ param_desc = _extract_param_description(func, param_name)
85
+ if param_desc:
86
+ prop_schema["description"] = param_desc
87
+
88
+ # Add default value if present
89
+ if param.default is not inspect.Parameter.empty:
90
+ if param.default is not None:
91
+ prop_schema["default"] = param.default
92
+ else:
93
+ # No default means required
94
+ schema["required"].append(param_name)
95
+
96
+ schema["properties"][param_name] = prop_schema
97
+
98
+ # Remove empty required list
99
+ if not schema["required"]:
100
+ del schema["required"]
101
+
102
+ return schema
103
+
104
+
105
+ def _type_to_schema(type_hint: Any) -> dict[str, Any]:
106
+ """
107
+ Convert a Python type hint to JSON Schema.
108
+
109
+ Args:
110
+ type_hint: The Python type hint.
111
+
112
+ Returns:
113
+ A JSON Schema dictionary for the type.
114
+ """
115
+ # Handle None/NoneType
116
+ if type_hint is None or type_hint is type(None):
117
+ return {"type": "null"}
118
+
119
+ # Handle basic types
120
+ if type_hint is str:
121
+ return {"type": "string"}
122
+ if type_hint is int:
123
+ return {"type": "integer"}
124
+ if type_hint is float:
125
+ return {"type": "number"}
126
+ if type_hint is bool:
127
+ return {"type": "boolean"}
128
+ if type_hint is Any:
129
+ return {} # Any type
130
+
131
+ # Handle generic types
132
+ origin = get_origin(type_hint)
133
+ args = get_args(type_hint)
134
+
135
+ # Handle Optional (Union[X, None])
136
+ if origin is Union:
137
+ # Filter out NoneType
138
+ non_none_args = [a for a in args if a is not type(None)]
139
+ if len(non_none_args) == 1:
140
+ # Optional[X] -> X with nullable
141
+ schema = _type_to_schema(non_none_args[0])
142
+ # JSON Schema draft 2020-12 uses "type": ["string", "null"]
143
+ # But for compatibility, we'll just return the base type
144
+ return schema
145
+ else:
146
+ # Union of multiple types
147
+ return {"oneOf": [_type_to_schema(a) for a in non_none_args]}
148
+
149
+ # Handle list/List
150
+ if origin is list:
151
+ schema: dict[str, Any] = {"type": "array"}
152
+ if args:
153
+ schema["items"] = _type_to_schema(args[0])
154
+ return schema
155
+
156
+ # Handle dict/Dict
157
+ if origin is dict:
158
+ schema = {"type": "object"}
159
+ if len(args) >= 2:
160
+ schema["additionalProperties"] = _type_to_schema(args[1])
161
+ return schema
162
+
163
+ # Handle tuple/Tuple
164
+ if origin is tuple:
165
+ if args:
166
+ if len(args) == 2 and args[1] is Ellipsis:
167
+ # Tuple[X, ...] is like List[X]
168
+ return {"type": "array", "items": _type_to_schema(args[0])}
169
+ else:
170
+ # Fixed-length tuple
171
+ return {
172
+ "type": "array",
173
+ "prefixItems": [_type_to_schema(a) for a in args],
174
+ "minItems": len(args),
175
+ "maxItems": len(args),
176
+ }
177
+ return {"type": "array"}
178
+
179
+ # Handle set/Set
180
+ if origin is set or origin is frozenset:
181
+ schema = {"type": "array", "uniqueItems": True}
182
+ if args:
183
+ schema["items"] = _type_to_schema(args[0])
184
+ return schema
185
+
186
+ # Handle Literal
187
+ try:
188
+ from typing import Literal
189
+
190
+ if origin is Literal:
191
+ return {"enum": list(args)}
192
+ except ImportError:
193
+ pass
194
+
195
+ # Handle classes with __annotations__ (dataclasses, etc.)
196
+ if hasattr(type_hint, "__annotations__"):
197
+ properties = {}
198
+ required = []
199
+ annotations = getattr(type_hint, "__annotations__", {})
200
+ for field_name, field_type in annotations.items():
201
+ properties[field_name] = _type_to_schema(field_type)
202
+ # For dataclasses, check if field has default
203
+ if hasattr(type_hint, "__dataclass_fields__"):
204
+ field_info = type_hint.__dataclass_fields__.get(field_name)
205
+ if field_info and field_info.default is field_info.default_factory:
206
+ required.append(field_name)
207
+ else:
208
+ required.append(field_name)
209
+
210
+ schema = {"type": "object", "properties": properties}
211
+ if required:
212
+ schema["required"] = required
213
+ return schema
214
+
215
+ # Default to string for unknown types
216
+ return {"type": "string"}
217
+
218
+
219
+ def _extract_param_description(func: Callable[..., Any], param_name: str) -> str | None:
220
+ """
221
+ Extract parameter description from function docstring.
222
+
223
+ Supports Google, NumPy, and Sphinx docstring formats.
224
+
225
+ Args:
226
+ func: The function with docstring.
227
+ param_name: The parameter name to look for.
228
+
229
+ Returns:
230
+ The parameter description or None.
231
+ """
232
+ docstring = func.__doc__
233
+ if not docstring:
234
+ return None
235
+
236
+ lines = docstring.split("\n")
237
+
238
+ # Try Google style: "param_name: description" or "param_name (type): description"
239
+ for i, line in enumerate(lines):
240
+ stripped = line.strip()
241
+ # Google style
242
+ if stripped.startswith(f"{param_name}:") or stripped.startswith(
243
+ f"{param_name} ("
244
+ ):
245
+ # Extract description after colon
246
+ if ":" in stripped:
247
+ desc = stripped.split(":", 1)[1].strip()
248
+ # Check for continuation on next lines
249
+ j = i + 1
250
+ while j < len(lines):
251
+ next_line = lines[j]
252
+ if next_line.strip() and not next_line.strip().startswith(
253
+ tuple("abcdefghijklmnopqrstuvwxyz_")
254
+ ):
255
+ # Continuation
256
+ desc += " " + next_line.strip()
257
+ j += 1
258
+ else:
259
+ break
260
+ return desc
261
+
262
+ # Sphinx style: ":param param_name: description"
263
+ if stripped.startswith(f":param {param_name}:"):
264
+ desc = stripped.split(":", 2)[2].strip()
265
+ return desc
266
+
267
+ # NumPy style: "param_name : type\n description"
268
+ if stripped == param_name or stripped.startswith(f"{param_name} :"):
269
+ if i + 1 < len(lines):
270
+ next_line = lines[i + 1].strip()
271
+ if next_line and not next_line.startswith(("-", "=")):
272
+ return next_line
273
+
274
+ return None
275
+
276
+
277
+ def tool(
278
+ name: str | None = None,
279
+ description: str | None = None,
280
+ category: ToolCategory = ToolCategory.CUSTOM,
281
+ risk_level: RiskLevel = RiskLevel.LOW,
282
+ requires_approval: bool = False,
283
+ timeout: float | None = None,
284
+ registry: ToolRegistry | None = None,
285
+ enabled: bool = True,
286
+ **metadata: Any,
287
+ ) -> Callable[[F], F]:
288
+ """
289
+ Decorator to register a function as a tool.
290
+
291
+ Automatically infers the parameter schema from type hints and
292
+ registers the function with the specified registry.
293
+
294
+ Args:
295
+ name: Tool name (defaults to function name).
296
+ description: Tool description (defaults to function docstring).
297
+ category: Tool category for organization.
298
+ risk_level: Risk level for authorization decisions.
299
+ requires_approval: Whether tool requires explicit approval.
300
+ timeout: Execution timeout in seconds.
301
+ registry: Registry to register with (defaults to global).
302
+ enabled: Whether tool is enabled by default.
303
+ **metadata: Additional metadata to attach.
304
+
305
+ Returns:
306
+ Decorator function.
307
+
308
+ Example:
309
+ >>> @tool(
310
+ ... name="search_web",
311
+ ... description="Search the web for information",
312
+ ... category=ToolCategory.SEARCH,
313
+ ... risk_level=RiskLevel.LOW,
314
+ ... )
315
+ ... def search_web(query: str, max_results: int = 10) -> list[dict]:
316
+ ... '''
317
+ ... Search the web.
318
+ ...
319
+ ... Args:
320
+ ... query: The search query.
321
+ ... max_results: Maximum results to return.
322
+ ... '''
323
+ ... return perform_search(query, max_results)
324
+ """
325
+
326
+ def decorator(func: F) -> F:
327
+ # Determine tool name
328
+ tool_name = name if name is not None else func.__name__
329
+
330
+ # Determine description
331
+ tool_description = description
332
+ if tool_description is None:
333
+ # Extract from docstring
334
+ if func.__doc__:
335
+ # Get first paragraph of docstring
336
+ doc_lines = func.__doc__.strip().split("\n\n")[0].split("\n")
337
+ tool_description = " ".join(line.strip() for line in doc_lines)
338
+ else:
339
+ tool_description = f"Execute {tool_name}"
340
+
341
+ # Infer parameter schema
342
+ parameters = infer_schema_from_function(func)
343
+
344
+ # Create tool definition
345
+ tool_def = ToolDefinition(
346
+ name=tool_name,
347
+ description=tool_description,
348
+ parameters=parameters,
349
+ category=category,
350
+ risk_level=risk_level,
351
+ requires_approval=requires_approval,
352
+ timeout=timeout,
353
+ handler=func,
354
+ metadata=metadata,
355
+ enabled=enabled,
356
+ )
357
+
358
+ # Register with registry
359
+ target_registry = registry if registry is not None else get_global_registry()
360
+ target_registry.register(tool_def)
361
+
362
+ logger.debug(f"Registered tool: {tool_name}")
363
+
364
+ # Preserve function metadata
365
+ @functools.wraps(func)
366
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
367
+ return func(*args, **kwargs)
368
+
369
+ # Attach tool definition to wrapper
370
+ wrapper._tool_definition = tool_def # type: ignore
371
+ wrapper._tool_name = tool_name # type: ignore
372
+
373
+ return wrapper # type: ignore
374
+
375
+ return decorator
376
+
377
+
378
+ def register_tool(
379
+ func: Callable[..., Any],
380
+ name: str | None = None,
381
+ description: str | None = None,
382
+ category: ToolCategory = ToolCategory.CUSTOM,
383
+ risk_level: RiskLevel = RiskLevel.LOW,
384
+ requires_approval: bool = False,
385
+ timeout: float | None = None,
386
+ registry: ToolRegistry | None = None,
387
+ enabled: bool = True,
388
+ **metadata: Any,
389
+ ) -> ToolDefinition:
390
+ """
391
+ Register a function as a tool without using decorator syntax.
392
+
393
+ Useful for registering existing functions or lambdas as tools.
394
+
395
+ Args:
396
+ func: The function to register.
397
+ name: Tool name (defaults to function name).
398
+ description: Tool description (defaults to function docstring).
399
+ category: Tool category for organization.
400
+ risk_level: Risk level for authorization decisions.
401
+ requires_approval: Whether tool requires explicit approval.
402
+ timeout: Execution timeout in seconds.
403
+ registry: Registry to register with (defaults to global).
404
+ enabled: Whether tool is enabled by default.
405
+ **metadata: Additional metadata to attach.
406
+
407
+ Returns:
408
+ The created ToolDefinition.
409
+
410
+ Example:
411
+ >>> def my_function(x: int) -> int:
412
+ ... return x * 2
413
+ >>> tool_def = register_tool(
414
+ ... my_function,
415
+ ... name="double",
416
+ ... description="Double a number",
417
+ ... )
418
+ """
419
+ # Determine tool name
420
+ tool_name = name if name is not None else func.__name__
421
+
422
+ # Determine description
423
+ tool_description = description
424
+ if tool_description is None:
425
+ if func.__doc__:
426
+ doc_lines = func.__doc__.strip().split("\n\n")[0].split("\n")
427
+ tool_description = " ".join(line.strip() for line in doc_lines)
428
+ else:
429
+ tool_description = f"Execute {tool_name}"
430
+
431
+ # Infer parameter schema
432
+ parameters = infer_schema_from_function(func)
433
+
434
+ # Create tool definition
435
+ tool_def = ToolDefinition(
436
+ name=tool_name,
437
+ description=tool_description,
438
+ parameters=parameters,
439
+ category=category,
440
+ risk_level=risk_level,
441
+ requires_approval=requires_approval,
442
+ timeout=timeout,
443
+ handler=func,
444
+ metadata=metadata,
445
+ enabled=enabled,
446
+ )
447
+
448
+ # Register with registry
449
+ target_registry = registry if registry is not None else get_global_registry()
450
+ target_registry.register(tool_def)
451
+
452
+ logger.debug(f"Registered tool: {tool_name}")
453
+
454
+ return tool_def
455
+
456
+
457
+ def unregister_tool(
458
+ name: str,
459
+ registry: ToolRegistry | None = None,
460
+ ) -> bool:
461
+ """
462
+ Unregister a tool by name.
463
+
464
+ Args:
465
+ name: The tool name to unregister.
466
+ registry: Registry to unregister from (defaults to global).
467
+
468
+ Returns:
469
+ True if the tool was found and unregistered.
470
+ """
471
+ target_registry = registry if registry is not None else get_global_registry()
472
+ return target_registry.unregister(name)
473
+
474
+
475
+ def get_tool_definition(func: Callable[..., Any]) -> ToolDefinition | None:
476
+ """
477
+ Get the tool definition attached to a decorated function.
478
+
479
+ Args:
480
+ func: The decorated function.
481
+
482
+ Returns:
483
+ The ToolDefinition or None if not a registered tool.
484
+
485
+ Example:
486
+ >>> @tool(name="my_tool")
487
+ ... def my_func():
488
+ ... pass
489
+ >>> tool_def = get_tool_definition(my_func)
490
+ >>> tool_def.name
491
+ 'my_tool'
492
+ """
493
+ return getattr(func, "_tool_definition", None)