loopy-agent 0.4.0__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.
loopy/__init__.py ADDED
@@ -0,0 +1,153 @@
1
+ """
2
+ Loopy — 8 Essential AI Concepts in one toolkit.
3
+
4
+ Modules:
5
+ loop - Agentic loop engine (Plan → Act → Observe → Reflect)
6
+ gateway - Multi-provider LLM routing with auth & rate limiting
7
+ guardrails - PII detection, jailbreak filtering, output safety
8
+ evals - Judge-based model evaluation framework
9
+ cache - Semantic token caching for cost optimization
10
+ observe - Traces, logs, and metrics for LLM observability
11
+ mcp - Model Context Protocol client
12
+ agents - Multi-agent orchestration with subagents
13
+
14
+ v0.2.0 additions:
15
+ - Evaluator-optimizer pattern (EvalGate, JudgeConfig)
16
+ - Orchestrator-workers pattern (Router, TaskDecomposer)
17
+ - Async context managers and connection pooling
18
+ - New middleware: Retry, CircuitBreaker, Fallback
19
+
20
+ v0.3.0 additions:
21
+ - OpenTelemetry export for traces/metrics
22
+ - First-party plugins: RAG, Tools, Memory
23
+ - Plugin marketplace support
24
+ """
25
+
26
+ from loopy.loop import AgentLoop, StepResult, LoopConfig, StepStatus
27
+ from loopy.gateway import Gateway, ModelProvider, ProviderConfig, GatewayResponse, ConnectionPool
28
+ from loopy.guardrails import GuardrailPipeline, InputFilter, OutputFilter, FilterAction
29
+ from loopy.evals import (
30
+ Evaluator, EvalCase, EvalResult, EvalSuite, Verdict,
31
+ EvalGate, EvalGateType, JudgeConfig, EvalGateResult,
32
+ )
33
+ from loopy.cache import LLMCache, CacheStats
34
+ from loopy.observe import Tracer, Span, SpanStatus, MetricsCollector
35
+ from loopy.mcp import MCPClient, Tool, LocalMCP
36
+ from loopy.agents import (
37
+ Orchestrator, SubAgent, AgentResult, AgentStatus,
38
+ Router, RoutingRule, TaskDecomposer, SubTask,
39
+ )
40
+ from loopy.middleware import (
41
+ Middleware,
42
+ MiddlewarePipeline,
43
+ MiddlewareContext,
44
+ FunctionMiddleware,
45
+ LoggingMiddleware,
46
+ TimingMiddleware,
47
+ RateLimitMiddleware,
48
+ CacheMiddleware,
49
+ ValidationMiddleware,
50
+ RetryMiddleware,
51
+ CircuitBreakerMiddleware,
52
+ FallbackMiddleware,
53
+ )
54
+ from loopy.plugins import Plugin, PluginRegistry, PluginLoader, PluginInfo
55
+
56
+ # First-party plugins (lazy import to avoid circular deps)
57
+ try:
58
+ from loopy.plugins.rag import RAGPlugin, Document, Retriever
59
+ from loopy.plugins.tools import ToolsPlugin, Tool, ToolResult
60
+ from loopy.plugins.memory import MemoryPlugin, Memory, MemoryStore
61
+ from loopy.plugins.audio import AudioPlugin, SpeechToText, TextToSpeech
62
+ from loopy.plugins.marketplace import MarketplacePlugin, PluginMarketplace
63
+ except ImportError:
64
+ pass # Optional dependencies
65
+
66
+ from loopy.observe import TraceExporter
67
+
68
+ __version__ = "0.4.0"
69
+
70
+ __all__ = [
71
+ # Agentic Loop
72
+ "AgentLoop",
73
+ "StepResult",
74
+ "LoopConfig",
75
+ "StepStatus",
76
+ # Gateway
77
+ "Gateway",
78
+ "ModelProvider",
79
+ "ProviderConfig",
80
+ "GatewayResponse",
81
+ "ConnectionPool",
82
+ # Guardrails
83
+ "GuardrailPipeline",
84
+ "InputFilter",
85
+ "OutputFilter",
86
+ "FilterAction",
87
+ # Evals (including v0.2.0 evaluator-optimizer)
88
+ "Evaluator",
89
+ "EvalCase",
90
+ "EvalResult",
91
+ "EvalSuite",
92
+ "Verdict",
93
+ "EvalGate",
94
+ "EvalGateType",
95
+ "JudgeConfig",
96
+ "EvalGateResult",
97
+ # Cache
98
+ "LLMCache",
99
+ "CacheStats",
100
+ # Observability
101
+ "Tracer",
102
+ "Span",
103
+ "SpanStatus",
104
+ "MetricsCollector",
105
+ # MCP
106
+ "MCPClient",
107
+ "Tool",
108
+ "LocalMCP",
109
+ # Multi-Agent (including v0.2.0 orchestrator-workers)
110
+ "Orchestrator",
111
+ "SubAgent",
112
+ "AgentResult",
113
+ "AgentStatus",
114
+ "Router",
115
+ "RoutingRule",
116
+ "TaskDecomposer",
117
+ "SubTask",
118
+ # Middleware (including v0.2.0 new middleware)
119
+ "Middleware",
120
+ "MiddlewarePipeline",
121
+ "MiddlewareContext",
122
+ "FunctionMiddleware",
123
+ "LoggingMiddleware",
124
+ "TimingMiddleware",
125
+ "RateLimitMiddleware",
126
+ "CacheMiddleware",
127
+ "ValidationMiddleware",
128
+ "RetryMiddleware",
129
+ "CircuitBreakerMiddleware",
130
+ "FallbackMiddleware",
131
+ # Plugins
132
+ "Plugin",
133
+ "PluginRegistry",
134
+ "PluginLoader",
135
+ "PluginInfo",
136
+ # First-party plugins
137
+ "RAGPlugin",
138
+ "Document",
139
+ "Retriever",
140
+ "ToolsPlugin",
141
+ "Tool",
142
+ "ToolResult",
143
+ "MemoryPlugin",
144
+ "Memory",
145
+ "MemoryStore",
146
+ "AudioPlugin",
147
+ "SpeechToText",
148
+ "TextToSpeech",
149
+ "MarketplacePlugin",
150
+ "PluginMarketplace",
151
+ # Observability exports
152
+ "TraceExporter",
153
+ ]
loopy/_types.pyi ADDED
@@ -0,0 +1,520 @@
1
+ """
2
+ Type stubs for loopy package.
3
+
4
+ Provides complete type hints for IDE autocompletion and static analysis.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import Any, Callable, Awaitable, Protocol, TypeVar, Generic, overload
10
+ from dataclasses import dataclass, field
11
+ from enum import Enum
12
+
13
+ T = TypeVar("T")
14
+
15
+ # ============================================================
16
+ # Loop Types
17
+ # ============================================================
18
+
19
+ class StepStatus(str, Enum):
20
+ PLANNING: str
21
+ ACTING: str
22
+ OBSERVING: str
23
+ REFLECTING: str
24
+ COMPLETE: str
25
+ FAILED: str
26
+
27
+ @dataclass
28
+ class StepResult:
29
+ step: int
30
+ status: StepStatus
31
+ plan: str
32
+ action: str
33
+ observation: str
34
+ reflection: str
35
+ data: dict[str, Any]
36
+ error: str | None
37
+
38
+ @dataclass
39
+ class LoopConfig:
40
+ max_steps: int
41
+ max_retries: int
42
+ stop_on_error: bool
43
+ planner: Callable[[list[StepResult]], Awaitable[str]] | None
44
+ actor: Callable[[str], Awaitable[str]] | None
45
+ observer: Callable[[str], Awaitable[str]] | None
46
+ reflector: Callable[[list[StepResult]], Awaitable[str]] | None
47
+ should_stop: Callable[[list[StepResult]], Awaitable[bool]] | None
48
+
49
+ class AgentLoop:
50
+ def __init__(self, config: LoopConfig | None = ...) -> None: ...
51
+ async def run(self, initial_context: str = ...) -> list[StepResult]: ...
52
+
53
+ # ============================================================
54
+ # Gateway Types
55
+ # ============================================================
56
+
57
+ class ModelProvider(str, Enum):
58
+ OPENAI: str
59
+ ANTHROPIC: str
60
+ OLLAMA: str
61
+ CUSTOM: str
62
+
63
+ @dataclass
64
+ class ProviderConfig:
65
+ provider: ModelProvider
66
+ api_key: str | None
67
+ base_url: str
68
+ model: str
69
+ rpm: int
70
+ tpm: int
71
+
72
+ @dataclass
73
+ class GatewayResponse:
74
+ content: str
75
+ model: str
76
+ provider: ModelProvider
77
+ tokens_used: int
78
+ latency_ms: float
79
+ cached: bool
80
+ metadata: dict[str, Any]
81
+
82
+ class Gateway:
83
+ def __init__(self) -> None: ...
84
+ def add_provider(self, name: str, config: ProviderConfig) -> None: ...
85
+ async def chat(
86
+ self,
87
+ message: str,
88
+ provider: str | None = ...,
89
+ system: str | None = ...,
90
+ temperature: float = ...,
91
+ max_tokens: int = ...,
92
+ **kwargs: Any,
93
+ ) -> GatewayResponse: ...
94
+ async def chat_batch(
95
+ self,
96
+ messages: list[str],
97
+ provider: str | None = ...,
98
+ system: str | None = ...,
99
+ temperature: float = ...,
100
+ max_tokens: int = ...,
101
+ max_concurrent: int = ...,
102
+ ) -> list[GatewayResponse]: ...
103
+ async def chat_streaming(
104
+ self,
105
+ message: str,
106
+ provider: str | None = ...,
107
+ system: str | None = ...,
108
+ temperature: float = ...,
109
+ max_tokens: int = ...,
110
+ ) -> Any: ... # AsyncGenerator[str, None]
111
+ def get_logs(self) -> list[dict[str, Any]]: ...
112
+ async def close(self) -> None: ...
113
+
114
+ # ============================================================
115
+ # Guardrails Types
116
+ # ============================================================
117
+
118
+ class FilterAction(str, Enum):
119
+ BLOCK: str
120
+ REDACT: str
121
+ WARN: str
122
+ PASS: str
123
+
124
+ @dataclass
125
+ class FilterResult:
126
+ action: FilterAction
127
+ original: str
128
+ filtered: str
129
+ reasons: list[str]
130
+ metadata: dict[str, Any]
131
+
132
+ @dataclass
133
+ class GuardrailConfig:
134
+ detect_ssn: bool
135
+ detect_email: bool
136
+ detect_phone: bool
137
+ detect_credit_card: bool
138
+ detect_ip_address: bool
139
+ detect_jailbreak: bool
140
+ jailbreak_sensitivity: float
141
+ blocked_patterns: list[str]
142
+ blocked_keywords: list[str]
143
+ custom_filters: list[Callable[[str], Awaitable[FilterResult]]]
144
+
145
+ class InputFilter:
146
+ def __init__(self, config: GuardrailConfig | None = ...) -> None: ...
147
+ def check(self, text: str) -> FilterResult: ...
148
+
149
+ class OutputFilter:
150
+ def __init__(self, config: GuardrailConfig | None = ...) -> None: ...
151
+ def check(self, text: str) -> FilterResult: ...
152
+
153
+ class GuardrailPipeline:
154
+ def __init__(self, config: GuardrailConfig | None = ...) -> None: ...
155
+ def filter_input(self, text: str) -> FilterResult: ...
156
+ def filter_output(self, text: str) -> FilterResult: ...
157
+ def get_history(self) -> list[dict[str, Any]]: ...
158
+
159
+ # ============================================================
160
+ # Evals Types
161
+ # ============================================================
162
+
163
+ class Verdict(str, Enum):
164
+ PASS: str
165
+ FAIL: str
166
+ PARTIAL: str
167
+
168
+ @dataclass
169
+ class EvalCase:
170
+ name: str
171
+ input_text: str
172
+ expected_output: str | None
173
+ criteria: list[str]
174
+ tags: list[str]
175
+ threshold: float
176
+
177
+ @dataclass
178
+ class EvalResult:
179
+ case: EvalCase
180
+ actual_output: str
181
+ verdict: Verdict
182
+ score: float
183
+ reasoning: str
184
+ criteria_scores: dict[str, float]
185
+ metadata: dict[str, Any]
186
+
187
+ @dataclass
188
+ class EvalSuite:
189
+ name: str
190
+ cases: list[EvalCase]
191
+ description: str
192
+
193
+ @dataclass
194
+ class EvalReport:
195
+ suite_name: str
196
+ results: list[EvalResult]
197
+ @property
198
+ def total(self) -> int: ...
199
+ @property
200
+ def passed(self) -> int: ...
201
+ @property
202
+ def failed(self) -> int: ...
203
+ @property
204
+ def partial(self) -> int: ...
205
+ @property
206
+ def pass_rate(self) -> float: ...
207
+ @property
208
+ def average_score(self) -> float: ...
209
+ def summary(self) -> dict[str, Any]: ...
210
+
211
+ class Evaluator:
212
+ def __init__(
213
+ self,
214
+ judge_fn: Callable[[str], Awaitable[str]] | None = ...,
215
+ model_fn: Callable[[str], Awaitable[str]] | None = ...,
216
+ ) -> None: ...
217
+ async def run(
218
+ self,
219
+ suite: EvalSuite,
220
+ model_fn: Callable[[str], Awaitable[str]] | None = ...,
221
+ ) -> EvalReport: ...
222
+
223
+ # ============================================================
224
+ # Cache Types
225
+ # ============================================================
226
+
227
+ @dataclass
228
+ class CacheEntry:
229
+ key: str
230
+ response: str
231
+ model: str
232
+ tokens_saved: int
233
+ created_at: float
234
+ last_accessed: float
235
+ access_count: int
236
+ metadata: dict[str, Any]
237
+
238
+ @dataclass
239
+ class CacheStats:
240
+ hits: int
241
+ misses: int
242
+ total_saved_tokens: int
243
+ @property
244
+ def hit_rate(self) -> float: ...
245
+ @property
246
+ def estimated_savings(self) -> float: ...
247
+
248
+ class LLMCache:
249
+ def __init__(
250
+ self,
251
+ ttl: int = ...,
252
+ max_size: int = ...,
253
+ persist_path: str | Path | None = ...,
254
+ ) -> None: ...
255
+ def get(self, prompt: str, model: str, **kwargs: Any) -> str | None: ...
256
+ def set(
257
+ self,
258
+ prompt: str,
259
+ response: str,
260
+ model: str,
261
+ tokens: int = ...,
262
+ **kwargs: Any,
263
+ ) -> None: ...
264
+ def invalidate(self, prompt: str, model: str, **kwargs: Any) -> bool: ...
265
+ def clear(self) -> None: ...
266
+ def stats(self) -> CacheStats: ...
267
+
268
+ # ============================================================
269
+ # Observability Types
270
+ # ============================================================
271
+
272
+ class SpanStatus(str, Enum):
273
+ OK: str
274
+ ERROR: str
275
+ UNSET: str
276
+
277
+ @dataclass
278
+ class Span:
279
+ name: str
280
+ trace_id: str
281
+ span_id: str
282
+ parent_id: str | None
283
+ start_time: float
284
+ end_time: float | None
285
+ status: SpanStatus
286
+ attributes: dict[str, Any]
287
+ events: list[dict[str, Any]]
288
+ @property
289
+ def duration_ms(self) -> float | None: ...
290
+ def set_attribute(self, key: str, value: Any) -> None: ...
291
+ def add_event(self, name: str, attributes: dict[str, Any] | None = ...) -> None: ...
292
+ def set_status(self, status: SpanStatus, message: str = ...) -> None: ...
293
+ def end(self) -> None: ...
294
+ def to_dict(self) -> dict[str, Any]: ...
295
+
296
+ class Tracer:
297
+ def __init__(self, service: str = ...) -> None: ...
298
+ def start_span(self, name: str, parent_id: str | None = ..., **attributes: Any) -> Span: ...
299
+ def start(self, name: str, **attributes: Any) -> SpanContext: ...
300
+ def get_spans(self) -> list[Span]: ...
301
+ def get_trace(self, trace_id: str) -> list[Span]: ...
302
+ def export_json(self) -> str: ...
303
+ def export_otlp(self) -> list[dict[str, Any]]: ...
304
+ def clear(self) -> None: ...
305
+
306
+ class SpanContext:
307
+ span: Span
308
+ def __enter__(self) -> Span: ...
309
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: ...
310
+
311
+ @dataclass
312
+ class MetricPoint:
313
+ name: str
314
+ value: float
315
+ timestamp: float
316
+ tags: dict[str, str]
317
+
318
+ class MetricsCollector:
319
+ def __init__(self) -> None: ...
320
+ def increment(self, name: str, value: float = ..., **tags: str) -> None: ...
321
+ def histogram(self, name: str, value: float, **tags: str) -> None: ...
322
+ def gauge(self, name: str, value: float, **tags: str) -> None: ...
323
+ def summary(self) -> dict[str, Any]: ...
324
+ def export(self) -> list[dict[str, Any]]: ...
325
+ def clear(self) -> None: ...
326
+
327
+ # ============================================================
328
+ # MCP Types
329
+ # ============================================================
330
+
331
+ @dataclass
332
+ class Tool:
333
+ name: str
334
+ description: str
335
+ input_schema: dict[str, Any]
336
+ annotations: dict[str, Any]
337
+
338
+ @dataclass
339
+ class ToolCall:
340
+ name: str
341
+ arguments: dict[str, Any]
342
+
343
+ @dataclass
344
+ class ToolResult:
345
+ content: str | list[dict[str, Any]]
346
+ is_error: bool
347
+ metadata: dict[str, Any]
348
+
349
+ class MCPClient:
350
+ def __init__(self, server_url: str, api_key: str | None = ...) -> None: ...
351
+ async def list_tools(self) -> list[Tool]: ...
352
+ async def call_tool(self, name: str, arguments: dict[str, Any] | None = ...) -> ToolResult: ...
353
+ async def health_check(self) -> bool: ...
354
+ async def close(self) -> None: ...
355
+
356
+ class LocalMCP:
357
+ def __init__(self) -> None: ...
358
+ def tool(
359
+ self,
360
+ name: str,
361
+ description: str = ...,
362
+ input_schema: dict[str, Any] | None = ...,
363
+ ) -> Callable: ...
364
+ async def list_tools(self) -> list[Tool]: ...
365
+ async def call_tool(self, name: str, arguments: dict[str, Any] | None = ...) -> ToolResult: ...
366
+
367
+ # ============================================================
368
+ # Agents Types
369
+ # ============================================================
370
+
371
+ class AgentStatus(str, Enum):
372
+ PENDING: str
373
+ RUNNING: str
374
+ COMPLETED: str
375
+ FAILED: str
376
+
377
+ @dataclass
378
+ class AgentResult:
379
+ agent_name: str
380
+ status: AgentStatus
381
+ output: str
382
+ error: str | None
383
+ metadata: dict[str, Any]
384
+ duration_ms: float
385
+
386
+ @dataclass
387
+ class SubAgent:
388
+ name: str
389
+ description: str
390
+ tools: list[str]
391
+ system_prompt: str
392
+ handler: Callable[[str, dict[str, Any]], Awaitable[str]] | None
393
+ status: AgentStatus
394
+ result: AgentResult | None
395
+
396
+ class Orchestrator:
397
+ def __init__(self, max_concurrent: int = ...) -> None: ...
398
+ def add_agent(self, agent: SubAgent) -> None: ...
399
+ def get_agent(self, name: str) -> SubAgent | None: ...
400
+ def list_agents(self) -> list[SubAgent]: ...
401
+ async def run(
402
+ self,
403
+ task: str,
404
+ agent_name: str | None = ...,
405
+ context: dict[str, Any] | None = ...,
406
+ ) -> AgentResult: ...
407
+ async def run_all(
408
+ self,
409
+ task: str,
410
+ context: dict[str, Any] | None = ...,
411
+ ) -> list[AgentResult]: ...
412
+ def get_history(self) -> list[AgentResult]: ...
413
+ def get_summary(self) -> dict[str, Any]: ...
414
+
415
+ # ============================================================
416
+ # Middleware Types
417
+ # ============================================================
418
+
419
+ @dataclass
420
+ class MiddlewareContext:
421
+ operation: str
422
+ data: dict[str, Any]
423
+ metadata: dict[str, Any]
424
+ cancelled: bool
425
+ cancel_reason: str
426
+ def cancel(self, reason: str = ...) -> None: ...
427
+
428
+ class Middleware(ABC):
429
+ @property
430
+ def name(self) -> str: ...
431
+ async def before(self, ctx: MiddlewareContext) -> MiddlewareContext: ...
432
+ async def after(self, ctx: MiddlewareContext, result: Any) -> Any: ...
433
+ async def on_error(self, ctx: MiddlewareContext, error: Exception) -> Exception: ...
434
+
435
+ class FunctionMiddleware(Middleware):
436
+ def __init__(
437
+ self,
438
+ name: str = ...,
439
+ before_fn: Callable[[MiddlewareContext], Awaitable[MiddlewareContext]] | None = ...,
440
+ after_fn: Callable[[MiddlewareContext, Any], Awaitable[Any]] | None = ...,
441
+ error_fn: Callable[[MiddlewareContext, Exception], Awaitable[Exception]] | None = ...,
442
+ ) -> None: ...
443
+
444
+ class MiddlewarePipeline:
445
+ def __init__(self) -> None: ...
446
+ def add(self, middleware: Middleware) -> None: ...
447
+ def remove(self, name: str) -> bool: ...
448
+ def clear(self) -> None: ...
449
+ async def execute(
450
+ self,
451
+ operation: str,
452
+ handler: Callable[..., Awaitable[Any]],
453
+ data: dict[str, Any] | None = ...,
454
+ **kwargs: Any,
455
+ ) -> Any: ...
456
+
457
+ class LoggingMiddleware(Middleware): ...
458
+ class TimingMiddleware(Middleware): ...
459
+
460
+ class RateLimitMiddleware(Middleware):
461
+ def __init__(self, max_per_second: int = ...) -> None: ...
462
+
463
+ class CacheMiddleware(Middleware):
464
+ def __init__(self, ttl: int = ...) -> None: ...
465
+
466
+ class ValidationMiddleware(Middleware):
467
+ def __init__(
468
+ self,
469
+ required_fields: list[str] | None = ...,
470
+ validators: dict[str, Callable[[Any], bool]] | None = ...,
471
+ ) -> None: ...
472
+
473
+ # ============================================================
474
+ # Plugin Types
475
+ # ============================================================
476
+
477
+ @dataclass
478
+ class PluginInfo:
479
+ name: str
480
+ version: str
481
+ description: str
482
+ author: str
483
+ url: str
484
+ capabilities: list[str]
485
+ requires: list[str]
486
+
487
+ class Plugin(ABC):
488
+ @property
489
+ @abstractmethod
490
+ def info(self) -> PluginInfo: ...
491
+ @abstractmethod
492
+ async def setup(self, registry: PluginRegistry) -> None: ...
493
+ async def teardown(self) -> None: ...
494
+
495
+ class PluginRegistry:
496
+ def __init__(self) -> None: ...
497
+ async def load(self, plugin: Plugin) -> None: ...
498
+ async def load_package(self, module_path: str) -> None: ...
499
+ async def load_directory(self, directory: str | Path) -> int: ...
500
+ def register_tool(self, name: str, handler: Callable) -> None: ...
501
+ def get_tool(self, name: str) -> Callable | None: ...
502
+ def list_tools(self) -> list[str]: ...
503
+ def register_middleware(self, name: str, middleware: Any) -> None: ...
504
+ def get_middleware(self, name: str) -> Any: ...
505
+ def register_provider(self, name: str, provider: Any) -> None: ...
506
+ def get_provider(self, name: str) -> Any: ...
507
+ def register_extension(self, hook_name: str, callback: Callable) -> None: ...
508
+ async def trigger_extension(self, hook_name: str, *args: Any, **kwargs: Any) -> list[Any]: ...
509
+ def get_plugin(self, name: str) -> Plugin | None: ...
510
+ def list_plugins(self) -> list[PluginInfo]: ...
511
+ async def unload(self, name: str) -> bool: ...
512
+ async def unload_all(self) -> None: ...
513
+
514
+ class PluginLoader:
515
+ def __init__(self, registry: PluginRegistry | None = ...) -> None: ...
516
+ async def discover(
517
+ self,
518
+ package: str | None = ...,
519
+ directory: str | Path | None = ...,
520
+ ) -> int: ...