minder-cli 0.2.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.
Files changed (132) hide show
  1. minder/__init__.py +12 -0
  2. minder/api/routers/prompts.py +177 -0
  3. minder/application/__init__.py +1 -0
  4. minder/application/admin/__init__.py +11 -0
  5. minder/application/admin/dto.py +453 -0
  6. minder/application/admin/jobs.py +327 -0
  7. minder/application/admin/use_cases.py +1895 -0
  8. minder/auth/__init__.py +12 -0
  9. minder/auth/context.py +26 -0
  10. minder/auth/middleware.py +70 -0
  11. minder/auth/principal.py +59 -0
  12. minder/auth/rate_limiter.py +89 -0
  13. minder/auth/rbac.py +60 -0
  14. minder/auth/service.py +541 -0
  15. minder/bootstrap/__init__.py +9 -0
  16. minder/bootstrap/providers.py +109 -0
  17. minder/bootstrap/transport.py +807 -0
  18. minder/cache/__init__.py +10 -0
  19. minder/cache/providers.py +140 -0
  20. minder/chunking/__init__.py +4 -0
  21. minder/chunking/code_splitter.py +184 -0
  22. minder/chunking/splitter.py +136 -0
  23. minder/cli.py +1542 -0
  24. minder/config.py +179 -0
  25. minder/continuity.py +363 -0
  26. minder/dev.py +160 -0
  27. minder/embedding/__init__.py +9 -0
  28. minder/embedding/base.py +7 -0
  29. minder/embedding/local.py +65 -0
  30. minder/embedding/openai.py +7 -0
  31. minder/graph/__init__.py +11 -0
  32. minder/graph/edges.py +13 -0
  33. minder/graph/executor.py +127 -0
  34. minder/graph/graph.py +263 -0
  35. minder/graph/nodes/__init__.py +27 -0
  36. minder/graph/nodes/evaluator.py +21 -0
  37. minder/graph/nodes/guard.py +64 -0
  38. minder/graph/nodes/llm.py +59 -0
  39. minder/graph/nodes/planning.py +30 -0
  40. minder/graph/nodes/reasoning.py +87 -0
  41. minder/graph/nodes/reranker.py +141 -0
  42. minder/graph/nodes/retriever.py +86 -0
  43. minder/graph/nodes/verification.py +230 -0
  44. minder/graph/nodes/workflow_planner.py +250 -0
  45. minder/graph/runtime.py +15 -0
  46. minder/graph/state.py +26 -0
  47. minder/llm/__init__.py +5 -0
  48. minder/llm/base.py +14 -0
  49. minder/llm/local.py +381 -0
  50. minder/llm/openai.py +89 -0
  51. minder/models/__init__.py +109 -0
  52. minder/models/base.py +10 -0
  53. minder/models/client.py +137 -0
  54. minder/models/document.py +34 -0
  55. minder/models/error.py +32 -0
  56. minder/models/graph.py +114 -0
  57. minder/models/history.py +32 -0
  58. minder/models/job.py +62 -0
  59. minder/models/prompt.py +41 -0
  60. minder/models/repository.py +62 -0
  61. minder/models/rule.py +68 -0
  62. minder/models/session.py +51 -0
  63. minder/models/skill.py +52 -0
  64. minder/models/user.py +41 -0
  65. minder/models/workflow.py +35 -0
  66. minder/observability/__init__.py +57 -0
  67. minder/observability/audit.py +243 -0
  68. minder/observability/logging.py +253 -0
  69. minder/observability/metrics.py +448 -0
  70. minder/observability/tracing.py +215 -0
  71. minder/presentation/__init__.py +1 -0
  72. minder/presentation/http/__init__.py +1 -0
  73. minder/presentation/http/admin/__init__.py +3 -0
  74. minder/presentation/http/admin/api.py +1309 -0
  75. minder/presentation/http/admin/context.py +94 -0
  76. minder/presentation/http/admin/dashboard.py +111 -0
  77. minder/presentation/http/admin/jobs.py +208 -0
  78. minder/presentation/http/admin/memories.py +185 -0
  79. minder/presentation/http/admin/prompts.py +219 -0
  80. minder/presentation/http/admin/routes.py +127 -0
  81. minder/presentation/http/admin/runtime.py +650 -0
  82. minder/presentation/http/admin/search.py +368 -0
  83. minder/presentation/http/admin/skills.py +230 -0
  84. minder/prompts/__init__.py +646 -0
  85. minder/prompts/formatter.py +142 -0
  86. minder/resources/__init__.py +318 -0
  87. minder/retrieval/__init__.py +5 -0
  88. minder/retrieval/hybrid.py +178 -0
  89. minder/retrieval/mmr.py +116 -0
  90. minder/retrieval/multi_hop.py +115 -0
  91. minder/runtime.py +15 -0
  92. minder/server.py +145 -0
  93. minder/store/__init__.py +64 -0
  94. minder/store/document.py +115 -0
  95. minder/store/error.py +82 -0
  96. minder/store/feedback.py +114 -0
  97. minder/store/graph.py +588 -0
  98. minder/store/history.py +57 -0
  99. minder/store/interfaces.py +512 -0
  100. minder/store/milvus/__init__.py +11 -0
  101. minder/store/milvus/client.py +26 -0
  102. minder/store/milvus/collections.py +15 -0
  103. minder/store/milvus/vector_store.py +232 -0
  104. minder/store/mongodb/__init__.py +11 -0
  105. minder/store/mongodb/client.py +49 -0
  106. minder/store/mongodb/indexes.py +90 -0
  107. minder/store/mongodb/operational_store.py +993 -0
  108. minder/store/relational.py +1087 -0
  109. minder/store/repo_state.py +58 -0
  110. minder/store/rule.py +93 -0
  111. minder/store/vector.py +79 -0
  112. minder/tools/__init__.py +47 -0
  113. minder/tools/auth.py +94 -0
  114. minder/tools/graph.py +839 -0
  115. minder/tools/ingest.py +353 -0
  116. minder/tools/memory.py +381 -0
  117. minder/tools/query.py +307 -0
  118. minder/tools/registry.py +269 -0
  119. minder/tools/repo_scanner.py +1266 -0
  120. minder/tools/search.py +15 -0
  121. minder/tools/session.py +316 -0
  122. minder/tools/skills.py +899 -0
  123. minder/tools/workflow.py +215 -0
  124. minder/transport/__init__.py +4 -0
  125. minder/transport/base.py +286 -0
  126. minder/transport/sse.py +252 -0
  127. minder/transport/stdio.py +29 -0
  128. minder_cli-0.2.0.dist-info/METADATA +318 -0
  129. minder_cli-0.2.0.dist-info/RECORD +132 -0
  130. minder_cli-0.2.0.dist-info/WHEEL +4 -0
  131. minder_cli-0.2.0.dist-info/entry_points.txt +2 -0
  132. minder_cli-0.2.0.dist-info/licenses/LICENSE +201 -0
@@ -0,0 +1,215 @@
1
+ from __future__ import annotations
2
+
3
+ import uuid
4
+ from typing import Any
5
+
6
+ from minder.continuity import build_continuity_brief, build_instruction_envelope
7
+ from minder.observability.metrics import (
8
+ record_continuity_gate,
9
+ record_continuity_packet,
10
+ )
11
+ from minder.store.interfaces import IOperationalStore
12
+ from minder.store.repo_state import RepoStateStore
13
+
14
+
15
+ class WorkflowTools:
16
+ def __init__(
17
+ self, store: IOperationalStore, repo_state_store: RepoStateStore
18
+ ) -> None:
19
+ self._store = store
20
+ self._repo_state = repo_state_store
21
+
22
+ async def minder_workflow_get(
23
+ self, *, repo_id: uuid.UUID, repo_path: str
24
+ ) -> dict[str, Any]:
25
+ repo = await self._require_repo(repo_id)
26
+ workflow = await self._require_workflow(repo.workflow_id)
27
+ state = await self._store.get_workflow_state_by_repo(repo_id)
28
+ relationships = (
29
+ dict(repo.relationships) if isinstance(repo.relationships, dict) else {}
30
+ )
31
+ await self._repo_state.write_relationships(repo_path, relationships)
32
+ if state is not None:
33
+ await self._repo_state.write_workflow_state(
34
+ repo_path,
35
+ {
36
+ "current_step": state.current_step,
37
+ "completed_steps": list(state.completed_steps),
38
+ "blocked_by": list(state.blocked_by),
39
+ "next_step": state.next_step,
40
+ },
41
+ )
42
+ return {
43
+ "workflow": {
44
+ "id": str(workflow.id),
45
+ "name": workflow.name,
46
+ "steps": list(workflow.steps),
47
+ "policies": dict(workflow.policies),
48
+ }
49
+ }
50
+
51
+ async def minder_workflow_step(
52
+ self, *, repo_id: uuid.UUID, repo_path: str
53
+ ) -> dict[str, Any]:
54
+ repo = await self._require_repo(repo_id)
55
+ workflow = await self._require_workflow(repo.workflow_id)
56
+ state = await self._require_workflow_state(repo_id)
57
+ payload = {
58
+ "current_step": state.current_step,
59
+ "completed_steps": list(state.completed_steps),
60
+ "blocked_by": list(state.blocked_by),
61
+ "next_step": state.next_step,
62
+ "instruction_envelope": build_instruction_envelope(
63
+ workflow=workflow,
64
+ workflow_state=state,
65
+ ),
66
+ }
67
+ await self._repo_state.write_workflow_state(repo_path, payload)
68
+ return payload
69
+
70
+ async def minder_workflow_update(
71
+ self,
72
+ *,
73
+ repo_id: uuid.UUID,
74
+ repo_path: str,
75
+ completed_step: str,
76
+ artifact_name: str | None = None,
77
+ artifact_content: str | None = None,
78
+ ) -> dict[str, Any]:
79
+ repo = await self._require_repo(repo_id)
80
+ workflow = await self._require_workflow(repo.workflow_id)
81
+ state = await self._require_workflow_state(repo_id)
82
+ step_names = [
83
+ step["name"]
84
+ for step in workflow.steps
85
+ if isinstance(step, dict) and "name" in step
86
+ ]
87
+ completed_steps = list(state.completed_steps)
88
+ if completed_step not in completed_steps:
89
+ completed_steps.append(completed_step)
90
+ next_step = self._next_step(completed_step, step_names)
91
+ artifacts = dict(state.artifacts)
92
+ if artifact_name and artifact_content is not None:
93
+ artifacts[artifact_name] = artifact_content
94
+ await self._repo_state.write_artifact(
95
+ repo_path, artifact_name, artifact_content
96
+ )
97
+ updated = await self._store.update_workflow_state(
98
+ state.id,
99
+ completed_steps=completed_steps,
100
+ current_step=next_step or completed_step,
101
+ next_step=self._next_step(next_step or completed_step, step_names),
102
+ artifacts=artifacts,
103
+ )
104
+ if updated is None:
105
+ raise ValueError(f"Workflow state not found for repo: {repo_id}")
106
+ await self._repo_state.write_workflow_state(
107
+ repo_path,
108
+ {
109
+ "current_step": updated.current_step,
110
+ "completed_steps": list(updated.completed_steps),
111
+ "blocked_by": list(updated.blocked_by),
112
+ "next_step": updated.next_step,
113
+ },
114
+ )
115
+ await self._repo_state.write_relationships(
116
+ repo_path,
117
+ dict(repo.relationships) if isinstance(repo.relationships, dict) else {},
118
+ )
119
+ return {
120
+ "current_step": updated.current_step,
121
+ "completed_steps": list(updated.completed_steps),
122
+ "next_step": updated.next_step,
123
+ "artifacts": dict(updated.artifacts),
124
+ }
125
+
126
+ async def minder_workflow_guard(
127
+ self,
128
+ *,
129
+ repo_id: uuid.UUID,
130
+ requested_step: str,
131
+ action: str | None = None,
132
+ ) -> dict[str, Any]:
133
+ repo = await self._require_repo(repo_id)
134
+ workflow = await self._require_workflow(repo.workflow_id)
135
+ state = await self._require_workflow_state(repo_id)
136
+ step_names = [
137
+ step["name"]
138
+ for step in workflow.steps
139
+ if isinstance(step, dict) and "name" in step
140
+ ]
141
+ expected_next = self._next_step(state.current_step, step_names)
142
+ allowed = requested_step == state.current_step or (
143
+ requested_step == expected_next
144
+ and state.current_step in list(state.completed_steps)
145
+ and not list(state.blocked_by)
146
+ )
147
+ violations: list[str] = []
148
+ if list(state.blocked_by):
149
+ violations.append("workflow_blocked")
150
+ if requested_step not in {state.current_step, expected_next}:
151
+ violations.append("step_skip_detected")
152
+ if action and requested_step != state.current_step:
153
+ violations.append("action_outside_current_step")
154
+ reason = (
155
+ None
156
+ if allowed
157
+ else f"Requested step '{requested_step}' is not allowed from '{state.current_step}'"
158
+ )
159
+ record_continuity_gate("passed" if allowed else "blocked")
160
+ record_continuity_packet("workflow_guard")
161
+ return {
162
+ "allowed": allowed,
163
+ "reason": reason,
164
+ "expected_next": expected_next,
165
+ "violations": violations,
166
+ "instruction_envelope": build_instruction_envelope(
167
+ workflow=workflow,
168
+ workflow_state=state,
169
+ ),
170
+ "continuity_brief": build_continuity_brief(
171
+ session=type(
172
+ "WorkflowSessionView",
173
+ (),
174
+ {
175
+ "id": getattr(state, "session_id", ""),
176
+ "state": {},
177
+ "project_context": {},
178
+ "active_skills": {},
179
+ },
180
+ )(),
181
+ workflow_state=state,
182
+ workflow=workflow,
183
+ ),
184
+ }
185
+
186
+ async def _require_repo(self, repo_id: uuid.UUID): # noqa: ANN202
187
+ repo = await self._store.get_repository_by_id(repo_id)
188
+ if repo is None:
189
+ raise ValueError(f"Repository not found: {repo_id}")
190
+ return repo
191
+
192
+ async def _require_workflow(self, workflow_id: uuid.UUID | None): # noqa: ANN202
193
+ if workflow_id is None:
194
+ raise ValueError("Repository has no workflow assigned")
195
+ workflow = await self._store.get_workflow_by_id(workflow_id)
196
+ if workflow is None:
197
+ raise ValueError(f"Workflow not found: {workflow_id}")
198
+ return workflow
199
+
200
+ async def _require_workflow_state(self, repo_id: uuid.UUID): # noqa: ANN202
201
+ state = await self._store.get_workflow_state_by_repo(repo_id)
202
+ if state is None:
203
+ raise ValueError(f"Workflow state not found for repo: {repo_id}")
204
+ return state
205
+
206
+ @staticmethod
207
+ def _next_step(current_step: str, steps: list[str]) -> str | None:
208
+ try:
209
+ index = steps.index(current_step)
210
+ except ValueError:
211
+ return steps[0] if steps else None
212
+ next_index = index + 1
213
+ if next_index >= len(steps):
214
+ return None
215
+ return steps[next_index]
@@ -0,0 +1,4 @@
1
+ from .sse import SSETransport
2
+ from .stdio import StdioTransport
3
+
4
+ __all__ = ["SSETransport", "StdioTransport"]
@@ -0,0 +1,286 @@
1
+ import logging
2
+ import inspect
3
+ from dataclasses import dataclass
4
+ from typing import Any, Callable
5
+
6
+ from mcp.server.fastmcp import FastMCP
7
+
8
+ from minder.auth.middleware import AuthMiddleware
9
+ from minder.auth.service import AuthError
10
+ from minder.auth.rate_limiter import RateLimiter
11
+ from minder.auth.principal import AdminUserPrincipal, ClientPrincipal, Principal
12
+ from minder.auth.service import AuthService
13
+ from minder.config import MinderConfig
14
+ from minder.auth.context import get_current_principal
15
+ from minder.store.interfaces import ICacheProvider, IOperationalStore
16
+ from minder.tools.registry import ALWAYS_AVAILABLE_FOR_CLIENTS
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+ ToolHandler = Callable[..., Any]
21
+
22
+
23
+ @dataclass(slots=True)
24
+ class RegisteredTool:
25
+ name: str
26
+ handler: ToolHandler
27
+ require_auth: bool
28
+ description: str | None = None
29
+
30
+
31
+ class BaseTransport:
32
+ transport_name = "base"
33
+
34
+ def __init__(
35
+ self,
36
+ *,
37
+ config: MinderConfig,
38
+ auth_service: AuthService | None = None,
39
+ cache_provider: ICacheProvider,
40
+ store: IOperationalStore | None = None,
41
+ ) -> None:
42
+ self._config = config
43
+ self._store = store
44
+ self._middleware = AuthMiddleware(auth_service) if auth_service is not None else None
45
+ self._rate_limiter = RateLimiter(cache=cache_provider, config=config)
46
+ self._server = FastMCP(
47
+ name=config.server.name,
48
+ host=config.server.host,
49
+ port=config.server.port,
50
+ )
51
+ self._tools: dict[str, RegisteredTool] = {}
52
+
53
+ @property
54
+ def app(self) -> FastMCP:
55
+ return self._server
56
+
57
+ def register_tool(
58
+ self,
59
+ name: str,
60
+ handler: ToolHandler,
61
+ *,
62
+ require_auth: bool = True,
63
+ description: str | None = None,
64
+ ) -> None:
65
+ effective_description = description or inspect.getdoc(handler) or self._describe_tool(name)
66
+ self._tools[name] = RegisteredTool(
67
+ name=name,
68
+ handler=handler,
69
+ require_auth=require_auth,
70
+ description=effective_description,
71
+ )
72
+
73
+ # We need to wrap the handler to inject auth logic, but we must
74
+ # preserve the original signature so FastMCP can generate the tool schema correctly.
75
+ import functools
76
+
77
+ @functools.wraps(handler)
78
+ async def wrapped_tool(*args: Any, **kwargs: Any) -> Any:
79
+ # Authorization might come from minder_authorization in MCP CallTool params
80
+ authorization = kwargs.pop("minder_authorization", None)
81
+ client_key = kwargs.pop("minder_client_key", None)
82
+
83
+ # Reconstruct arguments for call_tool
84
+ sig = inspect.signature(handler)
85
+ bound = sig.bind_partial(*args, **kwargs)
86
+ return await self.call_tool(
87
+ name,
88
+ arguments=bound.arguments,
89
+ authorization=authorization,
90
+ client_key=client_key,
91
+ )
92
+
93
+ # Modify the signature of wrapped_tool to remove 'user' parameter
94
+ # so FastMCP doesn't try to validate its presence in client requests.
95
+ orig_sig = inspect.signature(handler)
96
+ new_params = [
97
+ p for p in orig_sig.parameters.values()
98
+ if p.name not in {"user", "principal"}
99
+ ]
100
+ # New parameters to inject (must be before VAR_KEYWORD if present)
101
+ injected = [
102
+ inspect.Parameter(
103
+ "minder_authorization",
104
+ kind=inspect.Parameter.KEYWORD_ONLY,
105
+ default=None,
106
+ annotation=str | None,
107
+ ),
108
+ inspect.Parameter(
109
+ "minder_client_key",
110
+ kind=inspect.Parameter.KEYWORD_ONLY,
111
+ default=None,
112
+ annotation=str | None,
113
+ ),
114
+ ]
115
+
116
+ final_params = []
117
+ var_kw = None
118
+ for p in new_params:
119
+ if p.kind == inspect.Parameter.VAR_KEYWORD:
120
+ var_kw = p
121
+ else:
122
+ final_params.append(p)
123
+
124
+ final_params.extend(injected)
125
+ if var_kw:
126
+ final_params.append(var_kw)
127
+
128
+ wrapped_tool.__signature__ = orig_sig.replace(parameters=final_params) # type: ignore
129
+
130
+ self._server.add_tool(
131
+ wrapped_tool,
132
+ name=name,
133
+ description=effective_description,
134
+ structured_output=False,
135
+ )
136
+
137
+ def list_tools(self) -> list[str]:
138
+ return sorted(self._tools)
139
+
140
+ def _default_client_key(self) -> str | None:
141
+ return None
142
+
143
+ async def call_tool(
144
+ self,
145
+ name: str,
146
+ *,
147
+ arguments: dict[str, Any] | None = None,
148
+ authorization: str | None = None,
149
+ client_key: str | None = None,
150
+ ) -> Any:
151
+ if name not in self._tools:
152
+ raise KeyError(f"Unknown tool: {name}")
153
+
154
+ registered = self._tools[name]
155
+ kwargs = dict(arguments or {})
156
+ import time
157
+ start = time.perf_counter()
158
+ outcome = "error"
159
+ principal = None
160
+ try:
161
+ effective_client_key = client_key or self._default_client_key()
162
+ principal = await self._authenticate_if_required(registered, authorization, effective_client_key)
163
+ if principal is not None:
164
+ if (
165
+ isinstance(principal, ClientPrincipal)
166
+ and name not in ALWAYS_AVAILABLE_FOR_CLIENTS
167
+ and name not in principal.scopes
168
+ ):
169
+ outcome = "denied"
170
+ raise AuthError(
171
+ "AUTH_FORBIDDEN",
172
+ f"Client is not allowed to call tool '{name}'",
173
+ )
174
+ if self._rate_limiter.enabled():
175
+ await self._rate_limiter.enforce(principal=principal, tool_name=name)
176
+ kwargs["principal"] = principal
177
+ if isinstance(principal, AdminUserPrincipal) and principal.user is not None:
178
+ kwargs["user"] = principal.user
179
+
180
+ outcome = "success"
181
+ return await _invoke(registered.handler, **kwargs)
182
+ except Exception as exc:
183
+ if isinstance(exc, AuthError):
184
+ outcome = "denied"
185
+ elif outcome == "success": # Should not happen if we caught an exception
186
+ outcome = "error"
187
+ raise
188
+ finally:
189
+ elapsed = time.perf_counter() - start
190
+
191
+ client_id = "unknown"
192
+ actor_id = "unknown"
193
+ actor_type = "unknown"
194
+ if principal is not None:
195
+ client_id = getattr(principal, "client_slug") if hasattr(principal, "client_slug") else "unknown"
196
+ actor_id = str(principal.principal_id)
197
+ actor_type = principal.principal_type
198
+
199
+ # Record metrics (Prometheus - in-memory)
200
+ try:
201
+ from minder.observability.metrics import record_tool_call
202
+ record_tool_call(name, outcome, elapsed, client_id=client_id)
203
+ except Exception:
204
+ pass
205
+
206
+ # Record audit log (Persistent)
207
+ if self._store is not None:
208
+ try:
209
+ await self._store.create_audit_log(
210
+ actor_type=actor_type,
211
+ actor_id=actor_id,
212
+ event_type="tool_call",
213
+ tool_name=name,
214
+ resource_type="tool",
215
+ resource_id=name,
216
+ outcome=outcome,
217
+ audit_metadata={
218
+ "client_id": client_id,
219
+ "latency_ms": round(elapsed * 1000, 2),
220
+ "arguments": arguments,
221
+ }
222
+ )
223
+ except Exception:
224
+ # Don't let audit logging fail the whole tool call
225
+ pass
226
+
227
+ @staticmethod
228
+ def _describe_tool(name: str) -> str:
229
+ words = name.replace("minder_", "").replace("_", " ")
230
+ return f"Run the {words} tool in Minder."
231
+
232
+ async def _authenticate_if_required(
233
+ self,
234
+ registered: RegisteredTool,
235
+ authorization: str | None,
236
+ client_key: str | None,
237
+ ) -> Principal | None:
238
+ if not registered.require_auth:
239
+ return None
240
+
241
+ # 1. Try context first (set by middleware or previous call in same task)
242
+ context_principal = get_current_principal()
243
+ if context_principal is not None:
244
+ logger.info(
245
+ "BaseTransport: found principal in context: %s",
246
+ context_principal.principal_type,
247
+ )
248
+ return context_principal
249
+
250
+ # 2. Fallback to explicit authorization header if provided
251
+ if self._middleware is None:
252
+ logger.error("BaseTransport: Transport requires auth but no AuthService was configured")
253
+ raise RuntimeError("Transport requires auth but no AuthService was configured")
254
+
255
+ logger.debug(
256
+ "BaseTransport: no principal in context, checking auth header/client key",
257
+ )
258
+ principal = await self._middleware.authenticate_principal(
259
+ authorization,
260
+ client_key=client_key,
261
+ )
262
+ logger.info(
263
+ "BaseTransport: authenticated principal from request: %s",
264
+ principal.principal_type,
265
+ )
266
+ return principal
267
+
268
+
269
+ async def _invoke(handler: ToolHandler, **kwargs: Any) -> Any:
270
+ signature = inspect.signature(handler)
271
+ accepts_var_kw = any(
272
+ parameter.kind == inspect.Parameter.VAR_KEYWORD
273
+ for parameter in signature.parameters.values()
274
+ )
275
+ if accepts_var_kw:
276
+ filtered_kwargs = kwargs
277
+ else:
278
+ filtered_kwargs = {
279
+ key: value
280
+ for key, value in kwargs.items()
281
+ if key in signature.parameters
282
+ }
283
+ result = handler(**filtered_kwargs)
284
+ if inspect.isawaitable(result):
285
+ return await result
286
+ return result