fast-agent-mcp 0.0.7__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.

Potentially problematic release.


This version of fast-agent-mcp might be problematic. Click here for more details.

Files changed (100) hide show
  1. fast_agent_mcp-0.0.7.dist-info/METADATA +322 -0
  2. fast_agent_mcp-0.0.7.dist-info/RECORD +100 -0
  3. fast_agent_mcp-0.0.7.dist-info/WHEEL +4 -0
  4. fast_agent_mcp-0.0.7.dist-info/entry_points.txt +5 -0
  5. fast_agent_mcp-0.0.7.dist-info/licenses/LICENSE +201 -0
  6. mcp_agent/__init__.py +0 -0
  7. mcp_agent/agents/__init__.py +0 -0
  8. mcp_agent/agents/agent.py +277 -0
  9. mcp_agent/app.py +303 -0
  10. mcp_agent/cli/__init__.py +0 -0
  11. mcp_agent/cli/__main__.py +4 -0
  12. mcp_agent/cli/commands/bootstrap.py +221 -0
  13. mcp_agent/cli/commands/config.py +11 -0
  14. mcp_agent/cli/commands/setup.py +229 -0
  15. mcp_agent/cli/main.py +68 -0
  16. mcp_agent/cli/terminal.py +24 -0
  17. mcp_agent/config.py +334 -0
  18. mcp_agent/console.py +28 -0
  19. mcp_agent/context.py +251 -0
  20. mcp_agent/context_dependent.py +48 -0
  21. mcp_agent/core/fastagent.py +1013 -0
  22. mcp_agent/eval/__init__.py +0 -0
  23. mcp_agent/event_progress.py +88 -0
  24. mcp_agent/executor/__init__.py +0 -0
  25. mcp_agent/executor/decorator_registry.py +120 -0
  26. mcp_agent/executor/executor.py +293 -0
  27. mcp_agent/executor/task_registry.py +34 -0
  28. mcp_agent/executor/temporal.py +405 -0
  29. mcp_agent/executor/workflow.py +197 -0
  30. mcp_agent/executor/workflow_signal.py +325 -0
  31. mcp_agent/human_input/__init__.py +0 -0
  32. mcp_agent/human_input/handler.py +49 -0
  33. mcp_agent/human_input/types.py +58 -0
  34. mcp_agent/logging/__init__.py +0 -0
  35. mcp_agent/logging/events.py +123 -0
  36. mcp_agent/logging/json_serializer.py +163 -0
  37. mcp_agent/logging/listeners.py +216 -0
  38. mcp_agent/logging/logger.py +365 -0
  39. mcp_agent/logging/rich_progress.py +120 -0
  40. mcp_agent/logging/tracing.py +140 -0
  41. mcp_agent/logging/transport.py +461 -0
  42. mcp_agent/mcp/__init__.py +0 -0
  43. mcp_agent/mcp/gen_client.py +85 -0
  44. mcp_agent/mcp/mcp_activity.py +18 -0
  45. mcp_agent/mcp/mcp_agent_client_session.py +242 -0
  46. mcp_agent/mcp/mcp_agent_server.py +56 -0
  47. mcp_agent/mcp/mcp_aggregator.py +394 -0
  48. mcp_agent/mcp/mcp_connection_manager.py +330 -0
  49. mcp_agent/mcp/stdio.py +104 -0
  50. mcp_agent/mcp_server_registry.py +275 -0
  51. mcp_agent/progress_display.py +10 -0
  52. mcp_agent/resources/examples/decorator/main.py +26 -0
  53. mcp_agent/resources/examples/decorator/optimizer.py +78 -0
  54. mcp_agent/resources/examples/decorator/orchestrator.py +68 -0
  55. mcp_agent/resources/examples/decorator/parallel.py +81 -0
  56. mcp_agent/resources/examples/decorator/router.py +56 -0
  57. mcp_agent/resources/examples/decorator/tiny.py +22 -0
  58. mcp_agent/resources/examples/mcp_researcher/main-evalopt.py +53 -0
  59. mcp_agent/resources/examples/mcp_researcher/main.py +38 -0
  60. mcp_agent/telemetry/__init__.py +0 -0
  61. mcp_agent/telemetry/usage_tracking.py +18 -0
  62. mcp_agent/workflows/__init__.py +0 -0
  63. mcp_agent/workflows/embedding/__init__.py +0 -0
  64. mcp_agent/workflows/embedding/embedding_base.py +61 -0
  65. mcp_agent/workflows/embedding/embedding_cohere.py +49 -0
  66. mcp_agent/workflows/embedding/embedding_openai.py +46 -0
  67. mcp_agent/workflows/evaluator_optimizer/__init__.py +0 -0
  68. mcp_agent/workflows/evaluator_optimizer/evaluator_optimizer.py +359 -0
  69. mcp_agent/workflows/intent_classifier/__init__.py +0 -0
  70. mcp_agent/workflows/intent_classifier/intent_classifier_base.py +120 -0
  71. mcp_agent/workflows/intent_classifier/intent_classifier_embedding.py +134 -0
  72. mcp_agent/workflows/intent_classifier/intent_classifier_embedding_cohere.py +45 -0
  73. mcp_agent/workflows/intent_classifier/intent_classifier_embedding_openai.py +45 -0
  74. mcp_agent/workflows/intent_classifier/intent_classifier_llm.py +161 -0
  75. mcp_agent/workflows/intent_classifier/intent_classifier_llm_anthropic.py +60 -0
  76. mcp_agent/workflows/intent_classifier/intent_classifier_llm_openai.py +60 -0
  77. mcp_agent/workflows/llm/__init__.py +0 -0
  78. mcp_agent/workflows/llm/augmented_llm.py +645 -0
  79. mcp_agent/workflows/llm/augmented_llm_anthropic.py +539 -0
  80. mcp_agent/workflows/llm/augmented_llm_openai.py +615 -0
  81. mcp_agent/workflows/llm/llm_selector.py +345 -0
  82. mcp_agent/workflows/llm/model_factory.py +175 -0
  83. mcp_agent/workflows/orchestrator/__init__.py +0 -0
  84. mcp_agent/workflows/orchestrator/orchestrator.py +407 -0
  85. mcp_agent/workflows/orchestrator/orchestrator_models.py +154 -0
  86. mcp_agent/workflows/orchestrator/orchestrator_prompts.py +113 -0
  87. mcp_agent/workflows/parallel/__init__.py +0 -0
  88. mcp_agent/workflows/parallel/fan_in.py +350 -0
  89. mcp_agent/workflows/parallel/fan_out.py +187 -0
  90. mcp_agent/workflows/parallel/parallel_llm.py +141 -0
  91. mcp_agent/workflows/router/__init__.py +0 -0
  92. mcp_agent/workflows/router/router_base.py +276 -0
  93. mcp_agent/workflows/router/router_embedding.py +240 -0
  94. mcp_agent/workflows/router/router_embedding_cohere.py +59 -0
  95. mcp_agent/workflows/router/router_embedding_openai.py +59 -0
  96. mcp_agent/workflows/router/router_llm.py +301 -0
  97. mcp_agent/workflows/swarm/__init__.py +0 -0
  98. mcp_agent/workflows/swarm/swarm.py +320 -0
  99. mcp_agent/workflows/swarm/swarm_anthropic.py +42 -0
  100. mcp_agent/workflows/swarm/swarm_openai.py +41 -0
@@ -0,0 +1,325 @@
1
+ import asyncio
2
+ import uuid
3
+ from abc import abstractmethod, ABC
4
+ from typing import Any, Callable, Dict, Generic, List, Protocol, TypeVar
5
+
6
+ from pydantic import BaseModel, ConfigDict
7
+
8
+ SignalValueT = TypeVar("SignalValueT")
9
+
10
+ # TODO: saqadri - handle signals properly that works with other execution backends like Temporal as well
11
+
12
+
13
+ class Signal(BaseModel, Generic[SignalValueT]):
14
+ """Represents a signal that can be sent to a workflow."""
15
+
16
+ name: str
17
+ description: str | None = "Workflow Signal"
18
+ payload: SignalValueT | None = None
19
+ metadata: Dict[str, Any] | None = None
20
+ workflow_id: str | None = None
21
+
22
+ model_config = ConfigDict(arbitrary_types_allowed=True)
23
+
24
+
25
+ class SignalRegistration(BaseModel):
26
+ """Tracks registration of a signal handler."""
27
+
28
+ signal_name: str
29
+ unique_name: str
30
+ workflow_id: str | None = None
31
+
32
+ model_config = ConfigDict(arbitrary_types_allowed=True)
33
+
34
+
35
+ class SignalHandler(Protocol, Generic[SignalValueT]):
36
+ """Protocol for handling signals."""
37
+
38
+ @abstractmethod
39
+ async def signal(self, signal: Signal[SignalValueT]) -> None:
40
+ """Emit a signal to all waiting handlers and registered callbacks."""
41
+
42
+ @abstractmethod
43
+ async def wait_for_signal(
44
+ self,
45
+ signal: Signal[SignalValueT],
46
+ timeout_seconds: int | None = None,
47
+ ) -> SignalValueT:
48
+ """Wait for a signal to be emitted."""
49
+
50
+ def on_signal(self, signal_name: str) -> Callable:
51
+ """
52
+ Decorator to register a handler for a signal.
53
+
54
+ Example:
55
+ @signal_handler.on_signal("approval_needed")
56
+ async def handle_approval(value: str):
57
+ print(f"Got approval signal with value: {value}")
58
+ """
59
+
60
+
61
+ class PendingSignal(BaseModel):
62
+ """Tracks a waiting signal handler and its event."""
63
+
64
+ registration: SignalRegistration
65
+ event: asyncio.Event | None = None
66
+ value: SignalValueT | None = None
67
+
68
+ model_config = ConfigDict(arbitrary_types_allowed=True)
69
+
70
+
71
+ class BaseSignalHandler(ABC, Generic[SignalValueT]):
72
+ """Base class implementing common signal handling functionality."""
73
+
74
+ def __init__(self):
75
+ # Map signal_name -> list of PendingSignal objects
76
+ self._pending_signals: Dict[str, List[PendingSignal]] = {}
77
+ # Map signal_name -> list of (unique_name, handler) tuples
78
+ self._handlers: Dict[str, List[tuple[str, Callable]]] = {}
79
+ self._lock = asyncio.Lock()
80
+
81
+ async def cleanup(self, signal_name: str | None = None):
82
+ """Clean up handlers and registrations for a signal or all signals."""
83
+ async with self._lock:
84
+ if signal_name:
85
+ if signal_name in self._handlers:
86
+ del self._handlers[signal_name]
87
+ if signal_name in self._pending_signals:
88
+ del self._pending_signals[signal_name]
89
+ else:
90
+ self._handlers.clear()
91
+ self._pending_signals.clear()
92
+
93
+ def validate_signal(self, signal: Signal[SignalValueT]):
94
+ """Validate signal properties."""
95
+ if not signal.name:
96
+ raise ValueError("Signal name is required")
97
+ # Subclasses can override to add more validation
98
+
99
+ def on_signal(self, signal_name: str) -> Callable:
100
+ """Register a handler for a signal."""
101
+
102
+ def decorator(func: Callable) -> Callable:
103
+ unique_name = f"{signal_name}_{uuid.uuid4()}"
104
+
105
+ async def wrapped(value: SignalValueT):
106
+ try:
107
+ if asyncio.iscoroutinefunction(func):
108
+ await func(value)
109
+ else:
110
+ func(value)
111
+ except Exception as e:
112
+ # Log the error but don't fail the entire signal handling
113
+ print(f"Error in signal handler {signal_name}: {str(e)}")
114
+
115
+ self._handlers.setdefault(signal_name, []).append((unique_name, wrapped))
116
+ return wrapped
117
+
118
+ return decorator
119
+
120
+ @abstractmethod
121
+ async def signal(self, signal: Signal[SignalValueT]) -> None:
122
+ """Emit a signal to all waiting handlers and registered callbacks."""
123
+
124
+ @abstractmethod
125
+ async def wait_for_signal(
126
+ self,
127
+ signal: Signal[SignalValueT],
128
+ timeout_seconds: int | None = None,
129
+ ) -> SignalValueT:
130
+ """Wait for a signal to be emitted."""
131
+
132
+
133
+ class ConsoleSignalHandler(SignalHandler[str]):
134
+ """Simple console-based signal handling (blocks on input)."""
135
+
136
+ def __init__(self):
137
+ self._pending_signals: Dict[str, List[PendingSignal]] = {}
138
+ self._handlers: Dict[str, List[Callable]] = {}
139
+
140
+ async def wait_for_signal(self, signal, timeout_seconds=None):
141
+ """Block and wait for console input."""
142
+ print(f"\n[SIGNAL: {signal.name}] {signal.description}")
143
+ if timeout_seconds:
144
+ print(f"(Timeout in {timeout_seconds} seconds)")
145
+
146
+ # Use asyncio.get_event_loop().run_in_executor to make input non-blocking
147
+ loop = asyncio.get_event_loop()
148
+ if timeout_seconds is not None:
149
+ try:
150
+ value = await asyncio.wait_for(
151
+ loop.run_in_executor(None, input, "Enter value: "), timeout_seconds
152
+ )
153
+ except asyncio.TimeoutError:
154
+ print("\nTimeout waiting for input")
155
+ raise
156
+ else:
157
+ value = await loop.run_in_executor(None, input, "Enter value: ")
158
+
159
+ return value
160
+
161
+ # value = input(f"[SIGNAL: {signal.name}] {signal.description}: ")
162
+ # return value
163
+
164
+ def on_signal(self, signal_name):
165
+ def decorator(func):
166
+ async def wrapped(value: SignalValueT):
167
+ if asyncio.iscoroutinefunction(func):
168
+ await func(value)
169
+ else:
170
+ func(value)
171
+
172
+ self._handlers.setdefault(signal_name, []).append(wrapped)
173
+ return wrapped
174
+
175
+ return decorator
176
+
177
+ async def signal(self, signal):
178
+ print(f"[SIGNAL SENT: {signal.name}] Value: {signal.payload}")
179
+
180
+ handlers = self._handlers.get(signal.name, [])
181
+ await asyncio.gather(
182
+ *(handler(signal) for handler in handlers), return_exceptions=True
183
+ )
184
+
185
+ # Notify any waiting coroutines
186
+ if signal.name in self._pending_signals:
187
+ for ps in self._pending_signals[signal.name]:
188
+ ps.value = signal.payload
189
+ ps.event.set()
190
+
191
+
192
+ class AsyncioSignalHandler(BaseSignalHandler[SignalValueT]):
193
+ """
194
+ Asyncio-based signal handling using an internal dictionary of asyncio Events.
195
+ """
196
+
197
+ async def wait_for_signal(
198
+ self, signal, timeout_seconds: int | None = None
199
+ ) -> SignalValueT:
200
+ event = asyncio.Event()
201
+ unique_name = str(uuid.uuid4())
202
+
203
+ registration = SignalRegistration(
204
+ signal_name=signal.name,
205
+ unique_name=unique_name,
206
+ workflow_id=signal.workflow_id,
207
+ )
208
+
209
+ pending_signal = PendingSignal(registration=registration, event=event)
210
+
211
+ async with self._lock:
212
+ # Add to pending signals
213
+ self._pending_signals.setdefault(signal.name, []).append(pending_signal)
214
+
215
+ try:
216
+ # Wait for signal
217
+ if timeout_seconds is not None:
218
+ await asyncio.wait_for(event.wait(), timeout_seconds)
219
+ else:
220
+ await event.wait()
221
+
222
+ return pending_signal.value
223
+ except asyncio.TimeoutError as e:
224
+ raise TimeoutError(f"Timeout waiting for signal {signal.name}") from e
225
+ finally:
226
+ async with self._lock:
227
+ # Remove from pending signals
228
+ if signal.name in self._pending_signals:
229
+ self._pending_signals[signal.name] = [
230
+ ps
231
+ for ps in self._pending_signals[signal.name]
232
+ if ps.registration.unique_name != unique_name
233
+ ]
234
+ if not self._pending_signals[signal.name]:
235
+ del self._pending_signals[signal.name]
236
+
237
+ def on_signal(self, signal_name):
238
+ def decorator(func):
239
+ async def wrapped(value: SignalValueT):
240
+ if asyncio.iscoroutinefunction(func):
241
+ await func(value)
242
+ else:
243
+ func(value)
244
+
245
+ self._handlers.setdefault(signal_name, []).append(wrapped)
246
+ return wrapped
247
+
248
+ return decorator
249
+
250
+ async def signal(self, signal):
251
+ async with self._lock:
252
+ # Notify any waiting coroutines
253
+ if signal.name in self._pending_signals:
254
+ pending = self._pending_signals[signal.name]
255
+ for ps in pending:
256
+ ps.value = signal.payload
257
+ ps.event.set()
258
+
259
+ # Notify any registered handler functions
260
+ tasks = []
261
+ handlers = self._handlers.get(signal.name, [])
262
+ for _, handler in handlers:
263
+ tasks.append(handler(signal))
264
+
265
+ await asyncio.gather(*tasks, return_exceptions=True)
266
+
267
+
268
+ # TODO: saqadri - check if we need to do anything to combine this and AsyncioSignalHandler
269
+ class LocalSignalStore:
270
+ """
271
+ Simple in-memory structure that allows coroutines to wait for a signal
272
+ and triggers them when a signal is emitted.
273
+ """
274
+
275
+ def __init__(self):
276
+ # For each signal_name, store a list of futures that are waiting for it
277
+ self._waiters: Dict[str, List[asyncio.Future]] = {}
278
+
279
+ async def emit(self, signal_name: str, payload: Any):
280
+ # If we have waiting futures, set their result
281
+ if signal_name in self._waiters:
282
+ for future in self._waiters[signal_name]:
283
+ if not future.done():
284
+ future.set_result(payload)
285
+ self._waiters[signal_name].clear()
286
+
287
+ async def wait_for(
288
+ self, signal_name: str, timeout_seconds: int | None = None
289
+ ) -> Any:
290
+ loop = asyncio.get_running_loop()
291
+ future = loop.create_future()
292
+
293
+ self._waiters.setdefault(signal_name, []).append(future)
294
+
295
+ if timeout_seconds is not None:
296
+ try:
297
+ return await asyncio.wait_for(future, timeout=timeout_seconds)
298
+ except asyncio.TimeoutError:
299
+ # remove the fut from list
300
+ if not future.done():
301
+ self._waiters[signal_name].remove(future)
302
+ raise
303
+ else:
304
+ return await future
305
+
306
+
307
+ class SignalWaitCallback(Protocol):
308
+ """Protocol for callbacks that are triggered when a workflow pauses waiting for a given signal."""
309
+
310
+ async def __call__(
311
+ self,
312
+ signal_name: str,
313
+ request_id: str | None = None,
314
+ workflow_id: str | None = None,
315
+ metadata: Dict[str, Any] | None = None,
316
+ ) -> None:
317
+ """
318
+ Receive a notification that a workflow is pausing on a signal.
319
+
320
+ Args:
321
+ signal_name: The name of the signal the workflow is pausing on.
322
+ workflow_id: The ID of the workflow that is pausing (if using a workflow engine).
323
+ metadata: Additional metadata about the signal.
324
+ """
325
+ ...
File without changes
@@ -0,0 +1,49 @@
1
+ import asyncio
2
+ from rich.panel import Panel
3
+ from rich.prompt import Prompt
4
+
5
+ from mcp_agent.console import console
6
+ from mcp_agent.human_input.types import (
7
+ HumanInputRequest,
8
+ HumanInputResponse,
9
+ )
10
+ from mcp_agent.progress_display import progress_display
11
+
12
+
13
+ async def console_input_callback(request: HumanInputRequest) -> HumanInputResponse:
14
+ """Request input from a human user via console using rich panel and prompt."""
15
+
16
+ # Prepare the prompt text
17
+ prompt_text = request.prompt
18
+ if request.description:
19
+ prompt_text = f"[bold]{request.description}[/bold]\n\n{request.prompt}"
20
+
21
+ # Create a panel with the prompt
22
+ panel = Panel(
23
+ prompt_text,
24
+ title="[HUMAN INPUT REQUESTED]",
25
+ title_align="left",
26
+ style="green",
27
+ border_style="bold white",
28
+ padding=(1, 2),
29
+ )
30
+
31
+ # Use the context manager to pause the progress display while getting input
32
+ with progress_display.paused():
33
+ console.print(panel)
34
+
35
+ if request.timeout_seconds:
36
+ try:
37
+ loop = asyncio.get_event_loop()
38
+ response = await asyncio.wait_for(
39
+ loop.run_in_executor(None, lambda: Prompt.ask()),
40
+ request.timeout_seconds,
41
+ )
42
+ except asyncio.TimeoutError:
43
+ console.print("\n[red]Timeout waiting for input[/red]")
44
+ raise TimeoutError("No response received within timeout period")
45
+ else:
46
+ loop = asyncio.get_event_loop()
47
+ response = await loop.run_in_executor(None, lambda: Prompt.ask())
48
+
49
+ return HumanInputResponse(request_id=request.request_id, response=response.strip())
@@ -0,0 +1,58 @@
1
+ from typing import Any, AsyncIterator, Protocol
2
+ from pydantic import BaseModel
3
+
4
+ HUMAN_INPUT_SIGNAL_NAME = "__human_input__"
5
+
6
+
7
+ class HumanInputRequest(BaseModel):
8
+ """Represents a request for human input."""
9
+
10
+ prompt: str
11
+ """The prompt to show to the user"""
12
+
13
+ description: str | None = None
14
+ """Optional description of what the input is for"""
15
+
16
+ request_id: str | None = None
17
+ """Unique identifier for this request"""
18
+
19
+ workflow_id: str | None = None
20
+ """Optional workflow ID if using workflow engine"""
21
+
22
+ timeout_seconds: int | None = None
23
+ """Optional timeout in seconds"""
24
+
25
+ metadata: dict | None = None
26
+ """Additional request payload"""
27
+
28
+
29
+ class HumanInputResponse(BaseModel):
30
+ """Represents a response to a human input request"""
31
+
32
+ request_id: str
33
+ """ID of the original request"""
34
+
35
+ response: str
36
+ """The input provided by the human"""
37
+
38
+ metadata: dict[str, Any] | None = None
39
+ """Additional response payload"""
40
+
41
+
42
+ class HumanInputCallback(Protocol):
43
+ """Protocol for callbacks that handle human input requests."""
44
+
45
+ async def __call__(
46
+ self, request: HumanInputRequest
47
+ ) -> AsyncIterator[HumanInputResponse]:
48
+ """
49
+ Handle a human input request.
50
+
51
+ Args:
52
+ request: The input request to handle
53
+
54
+ Returns:
55
+ AsyncIterator yielding responses as they come in
56
+ TODO: saqadri - Keep it simple and just return HumanInputResponse?
57
+ """
58
+ ...
File without changes
@@ -0,0 +1,123 @@
1
+ """
2
+ Events and event filters for the logger module for the MCP Agent
3
+ """
4
+
5
+ import logging
6
+ import random
7
+
8
+ from datetime import datetime
9
+ from typing import (
10
+ Any,
11
+ Dict,
12
+ Literal,
13
+ Set,
14
+ )
15
+
16
+ from pydantic import BaseModel, ConfigDict, Field
17
+
18
+
19
+ EventType = Literal["debug", "info", "warning", "error", "progress"]
20
+ """Broad categories for events (severity or role)."""
21
+
22
+
23
+ class EventContext(BaseModel):
24
+ """
25
+ Stores correlation or cross-cutting data (workflow IDs, user IDs, etc.).
26
+ Also used for distributed environments or advanced logging.
27
+ """
28
+
29
+ session_id: str | None = None
30
+ workflow_id: str | None = None
31
+ # request_id: Optional[str] = None
32
+ # parent_event_id: Optional[str] = None
33
+ # correlation_id: Optional[str] = None
34
+ # user_id: Optional[str] = None
35
+
36
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
37
+
38
+
39
+ class Event(BaseModel):
40
+ """
41
+ Core event structure. Allows both a broad 'type' (EventType)
42
+ and a more specific 'name' string for domain-specific labeling (e.g. "ORDER_PLACED").
43
+ """
44
+
45
+ type: EventType
46
+ name: str | None = None
47
+ namespace: str
48
+ message: str
49
+ timestamp: datetime = Field(default_factory=datetime.now)
50
+ data: Dict[str, Any] = Field(default_factory=dict)
51
+ context: EventContext | None = None
52
+
53
+ # For distributed tracing
54
+ span_id: str | None = None
55
+ trace_id: str | None = None
56
+
57
+ model_config = ConfigDict(extra="allow", arbitrary_types_allowed=True)
58
+
59
+
60
+ class EventFilter(BaseModel):
61
+ """
62
+ Filter events by:
63
+ - allowed EventTypes (types)
64
+ - allowed event 'names'
65
+ - allowed namespace prefixes
66
+ - a minimum severity level (DEBUG < INFO < WARNING < ERROR)
67
+ """
68
+
69
+ types: Set[EventType] | None = Field(default_factory=set)
70
+ names: Set[str] | None = Field(default_factory=set)
71
+ namespaces: Set[str] | None = Field(default_factory=set)
72
+ min_level: EventType | None = "debug"
73
+
74
+ def matches(self, event: Event) -> bool:
75
+ """
76
+ Check if an event matches this EventFilter criteria.
77
+ """
78
+ # 1) Filter by broad event type
79
+ if self.types:
80
+ if event.type not in self.types:
81
+ return False
82
+
83
+ # 2) Filter by custom event name
84
+ if self.names:
85
+ if not event.name or event.name not in self.names:
86
+ return False
87
+
88
+ # 3) Filter by namespace prefix
89
+ if self.namespaces and not any(
90
+ event.namespace.startswith(ns) for ns in self.namespaces
91
+ ):
92
+ return False
93
+
94
+ # 4) Minimum severity
95
+ if self.min_level:
96
+ level_map: Dict[EventType, int] = {
97
+ "debug": logging.DEBUG,
98
+ "info": logging.INFO,
99
+ "warning": logging.WARNING,
100
+ "error": logging.ERROR,
101
+ }
102
+
103
+ min_val = level_map.get(self.min_level, logging.DEBUG)
104
+ event_val = level_map.get(event.type, logging.DEBUG)
105
+ if event_val < min_val:
106
+ return False
107
+
108
+ return True
109
+
110
+
111
+ class SamplingFilter(EventFilter):
112
+ """
113
+ Random sampling on top of base filter.
114
+ Only pass an event if it meets the base filter AND random() < sample_rate.
115
+ """
116
+
117
+ sample_rate: float = 0.1
118
+ """Fraction of events to pass through"""
119
+
120
+ def matches(self, event: Event) -> bool:
121
+ if not super().matches(event):
122
+ return False
123
+ return random.random() < self.sample_rate