humanrpc 0.1.0__tar.gz

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. humanrpc-0.1.0/PKG-INFO +66 -0
  2. humanrpc-0.1.0/README.md +45 -0
  3. humanrpc-0.1.0/humanrpc/__init__.py +8 -0
  4. humanrpc-0.1.0/humanrpc/__main__.py +145 -0
  5. humanrpc-0.1.0/humanrpc/client/__init__.py +0 -0
  6. humanrpc-0.1.0/humanrpc/client/client.py +46 -0
  7. humanrpc-0.1.0/humanrpc/client/endpoint.py +119 -0
  8. humanrpc-0.1.0/humanrpc/models/__init__.py +0 -0
  9. humanrpc-0.1.0/humanrpc/models/endpoint.py +14 -0
  10. humanrpc-0.1.0/humanrpc/models/request.py +13 -0
  11. humanrpc-0.1.0/humanrpc/models/response.py +33 -0
  12. humanrpc-0.1.0/humanrpc/server/__init__.py +0 -0
  13. humanrpc-0.1.0/humanrpc/server/app.py +37 -0
  14. humanrpc-0.1.0/humanrpc/server/manual.py +71 -0
  15. humanrpc-0.1.0/humanrpc/server/routes.py +110 -0
  16. humanrpc-0.1.0/humanrpc/server/state.py +52 -0
  17. humanrpc-0.1.0/humanrpc/server/ws.py +252 -0
  18. humanrpc-0.1.0/humanrpc/static/404/index.html +1 -0
  19. humanrpc-0.1.0/humanrpc/static/404.html +1 -0
  20. humanrpc-0.1.0/humanrpc/static/__next.__PAGE__.txt +9 -0
  21. humanrpc-0.1.0/humanrpc/static/__next._full.txt +20 -0
  22. humanrpc-0.1.0/humanrpc/static/__next._head.txt +6 -0
  23. humanrpc-0.1.0/humanrpc/static/__next._index.txt +5 -0
  24. humanrpc-0.1.0/humanrpc/static/__next._tree.txt +4 -0
  25. humanrpc-0.1.0/humanrpc/static/_next/static/JhNUkywfns7vp1sKB0uaY/_buildManifest.js +11 -0
  26. humanrpc-0.1.0/humanrpc/static/_next/static/JhNUkywfns7vp1sKB0uaY/_clientMiddlewareManifest.js +1 -0
  27. humanrpc-0.1.0/humanrpc/static/_next/static/JhNUkywfns7vp1sKB0uaY/_ssgManifest.js +1 -0
  28. humanrpc-0.1.0/humanrpc/static/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
  29. humanrpc-0.1.0/humanrpc/static/_next/static/chunks/158myu8e_yme3.js +1 -0
  30. humanrpc-0.1.0/humanrpc/static/_next/static/chunks/1jq4o6yq14o4c.js +31 -0
  31. humanrpc-0.1.0/humanrpc/static/_next/static/chunks/2imbn557mdp29.js +5 -0
  32. humanrpc-0.1.0/humanrpc/static/_next/static/chunks/2s6kossz229xl.css +3 -0
  33. humanrpc-0.1.0/humanrpc/static/_next/static/chunks/2zyxl_whwmmeo.js +1 -0
  34. humanrpc-0.1.0/humanrpc/static/_next/static/chunks/3n7dm2ojtyzwn.js +1 -0
  35. humanrpc-0.1.0/humanrpc/static/_next/static/chunks/turbopack-0_ef9nardpfaq.js +1 -0
  36. humanrpc-0.1.0/humanrpc/static/_next/static/media/4fa387ec64143e14-s.3f2jdebwxs8i-.woff2 +0 -0
  37. humanrpc-0.1.0/humanrpc/static/_next/static/media/53b9e256198e5412-s.390ncx5urfkfu.woff2 +0 -0
  38. humanrpc-0.1.0/humanrpc/static/_next/static/media/5ce348bf30bf5439-s.18ql67ww2ii1-.woff2 +0 -0
  39. humanrpc-0.1.0/humanrpc/static/_next/static/media/6306c77e7c8268e4-s.1ygs37po_4mpd.woff2 +0 -0
  40. humanrpc-0.1.0/humanrpc/static/_next/static/media/7178b3e590c64307-s.21jp631_3pja2.woff2 +0 -0
  41. humanrpc-0.1.0/humanrpc/static/_next/static/media/797e433ab948586e-s.p.0w5z4e7s8jfe5.woff2 +0 -0
  42. humanrpc-0.1.0/humanrpc/static/_next/static/media/7d817b4c03b0c5f1-s.2ojkkrs9oa5rc.woff2 +0 -0
  43. humanrpc-0.1.0/humanrpc/static/_next/static/media/8a480f0b521d4e75-s.1qq4vpdcun5oj.woff2 +0 -0
  44. humanrpc-0.1.0/humanrpc/static/_next/static/media/bbc41e54d2fcbd21-s.1_6ayb0k2-vor.woff2 +0 -0
  45. humanrpc-0.1.0/humanrpc/static/_next/static/media/caa3a2e1cccd8315-s.p.0wgildi0cnwt9.woff2 +0 -0
  46. humanrpc-0.1.0/humanrpc/static/_next/static/media/favicon.2vob68tjqpejf.ico +0 -0
  47. humanrpc-0.1.0/humanrpc/static/_next/static/media/fef07dbb0973bf53-s.3p2_lha1f2xer.woff2 +0 -0
  48. humanrpc-0.1.0/humanrpc/static/_not-found/__next._full.txt +16 -0
  49. humanrpc-0.1.0/humanrpc/static/_not-found/__next._head.txt +6 -0
  50. humanrpc-0.1.0/humanrpc/static/_not-found/__next._index.txt +5 -0
  51. humanrpc-0.1.0/humanrpc/static/_not-found/__next._not-found/__PAGE__.txt +5 -0
  52. humanrpc-0.1.0/humanrpc/static/_not-found/__next._not-found.txt +5 -0
  53. humanrpc-0.1.0/humanrpc/static/_not-found/__next._tree.txt +2 -0
  54. humanrpc-0.1.0/humanrpc/static/_not-found/index.html +1 -0
  55. humanrpc-0.1.0/humanrpc/static/_not-found/index.txt +16 -0
  56. humanrpc-0.1.0/humanrpc/static/favicon.ico +0 -0
  57. humanrpc-0.1.0/humanrpc/static/file.svg +1 -0
  58. humanrpc-0.1.0/humanrpc/static/globe.svg +1 -0
  59. humanrpc-0.1.0/humanrpc/static/index.html +1 -0
  60. humanrpc-0.1.0/humanrpc/static/index.txt +20 -0
  61. humanrpc-0.1.0/humanrpc/static/next.svg +1 -0
  62. humanrpc-0.1.0/humanrpc/static/vercel.svg +1 -0
  63. humanrpc-0.1.0/humanrpc/static/window.svg +1 -0
  64. humanrpc-0.1.0/pyproject.toml +42 -0
@@ -0,0 +1,66 @@
1
+ Metadata-Version: 2.4
2
+ Name: humanrpc
3
+ Version: 0.1.0
4
+ Summary: Human-powered typed RPC endpoints for building systems before the real implementation exists.
5
+ Author: cvaz1306
6
+ Author-email: christophervaz160@gmail.com
7
+ Requires-Python: >=3.12
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Requires-Dist: fastapi (>=0.136.3,<0.137.0)
13
+ Requires-Dist: httpx (>=0.28.1,<0.29.0)
14
+ Requires-Dist: pydantic (>=2.13.4,<3.0.0)
15
+ Requires-Dist: rich (>=15.0.0,<16.0.0)
16
+ Requires-Dist: typer (>=0.26.6,<0.27.0)
17
+ Requires-Dist: uvicorn (>=0.48.0,<0.49.0)
18
+ Requires-Dist: websockets (>=16.0,<17.0)
19
+ Description-Content-Type: text/markdown
20
+
21
+ # HumanRPC
22
+
23
+ HumanRPC is a human-powered RPC server for developing distributed systems before the real implementation exists.
24
+
25
+ Instead of calling an AI agent, microservice, planner, or capability, your application calls a typed endpoint. A human operator receives the request in a web UI, provides a response, and HumanRPC returns the result to the caller.
26
+
27
+ The goal is to enable rapid development of orchestration, routing, scheduling, memory, and workflow systems without waiting for every component to be implemented.
28
+
29
+ ## Features
30
+
31
+ * Pydantic-first API
32
+ * Typed request and response models
33
+ * Async and sync clients
34
+ * Human-powered endpoint implementations
35
+ * Exception simulation
36
+ * Timeout simulation
37
+ * Web UI for handling requests
38
+ * No external infrastructure required
39
+
40
+ ## Example
41
+
42
+ ```python
43
+ from humanrpc import Client
44
+
45
+ client = Client()
46
+
47
+ agent = client.endpoint(
48
+ name="agent",
49
+ input_model=ChatRequest,
50
+ response_model=ChatResponse,
51
+ exceptions=[
52
+ ValueError,
53
+ HTTPException,
54
+ ],
55
+ )
56
+
57
+ result = await agent.aask(
58
+ ChatRequest(...)
59
+ )
60
+ ```
61
+
62
+ ## Status
63
+
64
+ Early development.
65
+
66
+
@@ -0,0 +1,45 @@
1
+ # HumanRPC
2
+
3
+ HumanRPC is a human-powered RPC server for developing distributed systems before the real implementation exists.
4
+
5
+ Instead of calling an AI agent, microservice, planner, or capability, your application calls a typed endpoint. A human operator receives the request in a web UI, provides a response, and HumanRPC returns the result to the caller.
6
+
7
+ The goal is to enable rapid development of orchestration, routing, scheduling, memory, and workflow systems without waiting for every component to be implemented.
8
+
9
+ ## Features
10
+
11
+ * Pydantic-first API
12
+ * Typed request and response models
13
+ * Async and sync clients
14
+ * Human-powered endpoint implementations
15
+ * Exception simulation
16
+ * Timeout simulation
17
+ * Web UI for handling requests
18
+ * No external infrastructure required
19
+
20
+ ## Example
21
+
22
+ ```python
23
+ from humanrpc import Client
24
+
25
+ client = Client()
26
+
27
+ agent = client.endpoint(
28
+ name="agent",
29
+ input_model=ChatRequest,
30
+ response_model=ChatResponse,
31
+ exceptions=[
32
+ ValueError,
33
+ HTTPException,
34
+ ],
35
+ )
36
+
37
+ result = await agent.aask(
38
+ ChatRequest(...)
39
+ )
40
+ ```
41
+
42
+ ## Status
43
+
44
+ Early development.
45
+
@@ -0,0 +1,8 @@
1
+ from humanrpc.client.client import HumanRPCClient
2
+
3
+ Client = HumanRPCClient
4
+
5
+ __all__ = [
6
+ "Client",
7
+ "HumanRPCClient",
8
+ ]
@@ -0,0 +1,145 @@
1
+ import json
2
+ from pathlib import Path
3
+ from importlib.resources import files
4
+
5
+ import httpx
6
+ import typer
7
+ import uvicorn
8
+
9
+ from rich import print
10
+ from rich.table import Table
11
+
12
+ from humanrpc.server.app import create_app
13
+
14
+ cli = typer.Typer(help="HumanRPC CLI")
15
+
16
+ inspect_cli = typer.Typer(help="Inspect server state")
17
+ respond_cli = typer.Typer(help="Manually respond to requests")
18
+
19
+ cli.add_typer(inspect_cli, name="inspect")
20
+ cli.add_typer(respond_cli, name="respond")
21
+
22
+
23
+ @cli.command()
24
+ def serve(
25
+ host: str = "127.0.0.1",
26
+ port: int = 1078,
27
+ no_static: bool = False,
28
+ ):
29
+ app = create_app(
30
+ serve_static=not no_static,
31
+ )
32
+
33
+ uvicorn.run(
34
+ app,
35
+ host=host,
36
+ port=port,
37
+ reload=False,
38
+ )
39
+
40
+
41
+ @cli.command("static-path")
42
+ def static_path():
43
+ path = files("humanrpc").joinpath("static")
44
+ print(Path(path))
45
+
46
+
47
+ @inspect_cli.command("endpoints")
48
+ def inspect_endpoints(
49
+ endpoint: str = "http://127.0.0.1:1078",
50
+ ):
51
+ r = httpx.get(f"{endpoint}/endpoints")
52
+ data = r.json()
53
+
54
+ table = Table(title="Registered Endpoints")
55
+ table.add_column("Name")
56
+
57
+ for name in data.keys():
58
+ table.add_row(name)
59
+
60
+ print(table)
61
+
62
+
63
+ @inspect_cli.command("request")
64
+ def inspect_request(
65
+ request_id: str,
66
+ endpoint: str = "http://127.0.0.1:1078",
67
+ ):
68
+ r = httpx.get(f"{endpoint}/requests/{request_id}")
69
+ data = r.json()
70
+
71
+ print(data)
72
+
73
+
74
+ @inspect_cli.command("pending")
75
+ def inspect_pending(
76
+ endpoint: str = "http://127.0.0.1:1078",
77
+ ):
78
+ r = httpx.get(f"{endpoint}/requests/pending")
79
+ data = r.json()
80
+
81
+ print(data)
82
+
83
+
84
+ @respond_cli.command("success")
85
+ def respond_success(
86
+ request_id: str,
87
+ payload: str,
88
+ endpoint: str = "http://127.0.0.1:1078",
89
+ ):
90
+ data = json.loads(payload)
91
+
92
+ httpx.post(
93
+ f"{endpoint}/respond/success/{request_id}",
94
+ json=data,
95
+ )
96
+
97
+ print("[green]Response sent (success)[/green]")
98
+
99
+
100
+ @respond_cli.command("exception")
101
+ def respond_exception(
102
+ request_id: str,
103
+ exception_type: str,
104
+ payload: str,
105
+ endpoint: str = "http://127.0.0.1:1078",
106
+ ):
107
+ data = json.loads(payload)
108
+
109
+ httpx.post(
110
+ f"{endpoint}/respond/exception/{request_id}",
111
+ params={"exception_type": exception_type},
112
+ json=data,
113
+ )
114
+
115
+ print(
116
+ f"[yellow]Response sent (exception: {exception_type})[/yellow]"
117
+ )
118
+
119
+
120
+ @respond_cli.command("timeout")
121
+ def respond_timeout(
122
+ request_id: str,
123
+ endpoint: str = "http://127.0.0.1:1078",
124
+ ):
125
+ httpx.post(f"{endpoint}/respond/timeout/{request_id}")
126
+
127
+ print("[blue]Response sent (timeout)[/blue]")
128
+
129
+
130
+ @respond_cli.command("ignore")
131
+ def respond_ignore(
132
+ request_id: str,
133
+ endpoint: str = "http://127.0.0.1:1078",
134
+ ):
135
+ httpx.post(f"{endpoint}/respond/ignore/{request_id}")
136
+
137
+ print("[dim]Response sent (ignore)[/dim]")
138
+
139
+
140
+ def main():
141
+ cli()
142
+
143
+
144
+ if __name__ == "__main__":
145
+ main()
File without changes
@@ -0,0 +1,46 @@
1
+ import httpx
2
+
3
+ from humanrpc.client.endpoint import Endpoint
4
+ from humanrpc.models.endpoint import (
5
+ EndpointDefinition,
6
+ ExceptionDefinition,
7
+ )
8
+
9
+
10
+ class HumanRPCClient:
11
+ def __init__(self, base_url: str = "http://127.0.0.1:1078"):
12
+ self.base_url = base_url
13
+
14
+ def endpoint(
15
+ self,
16
+ *,
17
+ name: str,
18
+ input_model,
19
+ response_model,
20
+ exceptions=None,
21
+ ) -> Endpoint:
22
+ exceptions = exceptions or []
23
+
24
+ definition = EndpointDefinition(
25
+ name=name,
26
+ input_schema=input_model.model_json_schema(),
27
+ response_schema=response_model.model_json_schema(),
28
+ exceptions=[
29
+ ExceptionDefinition(name=exc.__name__)
30
+ for exc in exceptions
31
+ ],
32
+ )
33
+
34
+ httpx.post(
35
+ f"{self.base_url}/endpoints",
36
+ json=definition.model_dump(),
37
+ timeout=5.0,
38
+ )
39
+
40
+ return Endpoint(
41
+ name=name,
42
+ input_model=input_model,
43
+ response_model=response_model,
44
+ exceptions=exceptions,
45
+ base_url=self.base_url,
46
+ )
@@ -0,0 +1,119 @@
1
+ import uuid
2
+ import httpx
3
+ import asyncio
4
+ import json
5
+ import time
6
+ import websockets
7
+
8
+ from pydantic import BaseModel
9
+
10
+
11
+ class Endpoint:
12
+ def __init__(
13
+ self,
14
+ *,
15
+ name: str,
16
+ input_model,
17
+ response_model,
18
+ exceptions: list[type[Exception]],
19
+ base_url: str,
20
+ ):
21
+ self.name = name
22
+ self.input_model = input_model
23
+ self.response_model = response_model
24
+ self.exceptions = exceptions
25
+ self.base_url = base_url
26
+
27
+ async def aask(self, request: BaseModel):
28
+ payload = request.model_dump()
29
+ request_id = str(uuid.uuid4())
30
+
31
+ # Use AsyncClient to avoid blocking the loop
32
+ async with httpx.AsyncClient() as client:
33
+ await client.post(
34
+ f"{self.base_url}/requests",
35
+ json={
36
+ "request_id": request_id,
37
+ "endpoint_name": self.name,
38
+ "payload": payload,
39
+ },
40
+ timeout=5.0,
41
+ )
42
+
43
+ ws_url = self.base_url.replace("http", "ws")
44
+ ws_url = f"{ws_url}/ws/requests/{request_id}"
45
+
46
+ async with websockets.connect(ws_url) as ws:
47
+ await ws.send("ready")
48
+
49
+ while True:
50
+ msg = await ws.recv()
51
+ record = json.loads(msg)
52
+
53
+ response = record.get("response")
54
+ if not response:
55
+ continue
56
+
57
+ kind = response.get("kind")
58
+
59
+ if kind == "success":
60
+ return self.response_model.model_validate(response["payload"])
61
+
62
+ if kind == "exception":
63
+ exc_type = response["exception_type"]
64
+
65
+ for exc in self.exceptions:
66
+ if exc.__name__ == exc_type:
67
+ raise exc(**response["payload"])
68
+
69
+ raise Exception(f"Unknown exception: {exc_type}")
70
+
71
+ if kind == "timeout":
72
+ raise TimeoutError()
73
+
74
+ if kind == "ignore":
75
+ return None
76
+
77
+ def ask(self, request: BaseModel):
78
+ request_id = str(uuid.uuid4())
79
+
80
+ with httpx.Client() as client:
81
+ client.post(
82
+ f"{self.base_url}/requests",
83
+ json={
84
+ "request_id": request_id,
85
+ "endpoint_name": self.name,
86
+ "payload": request.model_dump(),
87
+ },
88
+ timeout=5.0,
89
+ )
90
+
91
+ # Poll for resolution
92
+ while True:
93
+ r = client.get(f"{self.base_url}/requests/{request_id}")
94
+ if r.status_code == 200:
95
+ record = r.json()
96
+ if record.get("status") == "completed":
97
+ response = record.get("response")
98
+ if not response:
99
+ raise Exception("Request completed with no response payload.")
100
+
101
+ kind = response.get("kind")
102
+
103
+ if kind == "success":
104
+ return self.response_model.model_validate(response["payload"])
105
+
106
+ if kind == "exception":
107
+ exc_type = response["exception_type"]
108
+ for exc in self.exceptions:
109
+ if exc.__name__ == exc_type:
110
+ raise exc(**response["payload"])
111
+ raise Exception(f"Unknown exception: {exc_type}")
112
+
113
+ if kind == "timeout":
114
+ raise TimeoutError()
115
+
116
+ if kind == "ignore":
117
+ return None
118
+
119
+ time.sleep(0.2)
File without changes
@@ -0,0 +1,14 @@
1
+ from pydantic import BaseModel, Field
2
+
3
+
4
+ class ExceptionDefinition(BaseModel):
5
+ name: str
6
+
7
+
8
+ class EndpointDefinition(BaseModel):
9
+ name: str
10
+
11
+ input_schema: dict
12
+ response_schema: dict
13
+
14
+ exceptions: list[ExceptionDefinition] = Field(default_factory=list)
@@ -0,0 +1,13 @@
1
+ from uuid import uuid4
2
+ from datetime import datetime
3
+ from pydantic import BaseModel, Field
4
+
5
+
6
+ class RequestEnvelope(BaseModel):
7
+ request_id: str = Field(default_factory=lambda: str(uuid4()))
8
+
9
+ endpoint_name: str
10
+
11
+ payload: dict
12
+
13
+ created_at: datetime = Field(default_factory=datetime.utcnow)
@@ -0,0 +1,33 @@
1
+ from typing import Literal
2
+
3
+ from pydantic import BaseModel
4
+
5
+
6
+ class SuccessResponse(BaseModel):
7
+ kind: Literal["success"] = "success"
8
+
9
+ payload: dict
10
+
11
+
12
+ class ExceptionResponse(BaseModel):
13
+ kind: Literal["exception"] = "exception"
14
+
15
+ exception_type: str
16
+
17
+ payload: dict
18
+
19
+
20
+ class TimeoutResponse(BaseModel):
21
+ kind: Literal["timeout"] = "timeout"
22
+
23
+
24
+ class IgnoreResponse(BaseModel):
25
+ kind: Literal["ignore"] = "ignore"
26
+
27
+ ResponseEnvelope = (
28
+ SuccessResponse
29
+ | ExceptionResponse
30
+ | TimeoutResponse
31
+ | IgnoreResponse
32
+ )
33
+
File without changes
@@ -0,0 +1,37 @@
1
+ from importlib.resources import files
2
+
3
+ from fastapi import FastAPI
4
+ from fastapi.staticfiles import StaticFiles
5
+
6
+ from starlette.routing import Mount
7
+
8
+ from humanrpc.server.routes import router as core_router
9
+ from humanrpc.server.manual import router as manual_router
10
+ from humanrpc.server.ws import router as ws_router
11
+
12
+
13
+ def create_app(*, serve_static: bool = True) -> FastAPI:
14
+ app = FastAPI()
15
+
16
+ app.include_router(ws_router)
17
+ app.include_router(core_router)
18
+ app.include_router(manual_router)
19
+
20
+ if serve_static:
21
+ static_dir = files("humanrpc").joinpath("static")
22
+
23
+ app.mount(
24
+ "/",
25
+ StaticFiles(
26
+ directory=str(static_dir),
27
+ html=True,
28
+ ),
29
+ name="static",
30
+ )
31
+
32
+ return app
33
+
34
+
35
+ app = create_app()
36
+
37
+ app.routes.sort(key=lambda r: 1 if isinstance(r, Mount) and r.path == "/" else 0)
@@ -0,0 +1,71 @@
1
+ from datetime import datetime
2
+ from fastapi import APIRouter
3
+
4
+ from humanrpc.server.state import state
5
+ from humanrpc.models.response import (
6
+ SuccessResponse,
7
+ ExceptionResponse,
8
+ TimeoutResponse,
9
+ IgnoreResponse,
10
+ )
11
+
12
+ router = APIRouter()
13
+
14
+
15
+ async def _complete(request_id: str):
16
+ await state.notify(request_id)
17
+
18
+
19
+ @router.post("/respond/success/{request_id}")
20
+ async def respond_success(request_id: str, payload: dict):
21
+ record = state.requests.get(request_id)
22
+
23
+ if record:
24
+ record.status = "completed"
25
+ record.response = SuccessResponse(payload=payload)
26
+ record.responded_at = datetime.utcnow()
27
+ await _complete(request_id)
28
+
29
+ return {"ok": True}
30
+
31
+
32
+ @router.post("/respond/exception/{request_id}")
33
+ async def respond_exception(request_id: str, exception_type: str, payload: dict):
34
+ record = state.requests.get(request_id)
35
+
36
+ if record:
37
+ record.status = "completed"
38
+ record.response = ExceptionResponse(
39
+ exception_type=exception_type,
40
+ payload=payload,
41
+ )
42
+ record.responded_at = datetime.utcnow()
43
+ await _complete(request_id)
44
+
45
+ return {"ok": True}
46
+
47
+
48
+ @router.post("/respond/timeout/{request_id}")
49
+ async def respond_timeout(request_id: str):
50
+ record = state.requests.get(request_id)
51
+
52
+ if record:
53
+ record.status = "completed"
54
+ record.response = TimeoutResponse()
55
+ record.responded_at = datetime.utcnow()
56
+ await _complete(request_id)
57
+
58
+ return {"ok": True}
59
+
60
+
61
+ @router.post("/respond/ignore/{request_id}")
62
+ async def respond_ignore(request_id: str):
63
+ record = state.requests.get(request_id)
64
+
65
+ if record:
66
+ record.status = "completed"
67
+ record.response = IgnoreResponse()
68
+ record.responded_at = datetime.utcnow()
69
+ await _complete(request_id)
70
+
71
+ return {"ok": True}