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.
- humanrpc-0.1.0/PKG-INFO +66 -0
- humanrpc-0.1.0/README.md +45 -0
- humanrpc-0.1.0/humanrpc/__init__.py +8 -0
- humanrpc-0.1.0/humanrpc/__main__.py +145 -0
- humanrpc-0.1.0/humanrpc/client/__init__.py +0 -0
- humanrpc-0.1.0/humanrpc/client/client.py +46 -0
- humanrpc-0.1.0/humanrpc/client/endpoint.py +119 -0
- humanrpc-0.1.0/humanrpc/models/__init__.py +0 -0
- humanrpc-0.1.0/humanrpc/models/endpoint.py +14 -0
- humanrpc-0.1.0/humanrpc/models/request.py +13 -0
- humanrpc-0.1.0/humanrpc/models/response.py +33 -0
- humanrpc-0.1.0/humanrpc/server/__init__.py +0 -0
- humanrpc-0.1.0/humanrpc/server/app.py +37 -0
- humanrpc-0.1.0/humanrpc/server/manual.py +71 -0
- humanrpc-0.1.0/humanrpc/server/routes.py +110 -0
- humanrpc-0.1.0/humanrpc/server/state.py +52 -0
- humanrpc-0.1.0/humanrpc/server/ws.py +252 -0
- humanrpc-0.1.0/humanrpc/static/404/index.html +1 -0
- humanrpc-0.1.0/humanrpc/static/404.html +1 -0
- humanrpc-0.1.0/humanrpc/static/__next.__PAGE__.txt +9 -0
- humanrpc-0.1.0/humanrpc/static/__next._full.txt +20 -0
- humanrpc-0.1.0/humanrpc/static/__next._head.txt +6 -0
- humanrpc-0.1.0/humanrpc/static/__next._index.txt +5 -0
- humanrpc-0.1.0/humanrpc/static/__next._tree.txt +4 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/JhNUkywfns7vp1sKB0uaY/_buildManifest.js +11 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/JhNUkywfns7vp1sKB0uaY/_clientMiddlewareManifest.js +1 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/JhNUkywfns7vp1sKB0uaY/_ssgManifest.js +1 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/chunks/0cz1d0mv5g_q7.js +1 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/chunks/158myu8e_yme3.js +1 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/chunks/1jq4o6yq14o4c.js +31 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/chunks/2imbn557mdp29.js +5 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/chunks/2s6kossz229xl.css +3 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/chunks/2zyxl_whwmmeo.js +1 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/chunks/3n7dm2ojtyzwn.js +1 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/chunks/turbopack-0_ef9nardpfaq.js +1 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/media/4fa387ec64143e14-s.3f2jdebwxs8i-.woff2 +0 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/media/53b9e256198e5412-s.390ncx5urfkfu.woff2 +0 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/media/5ce348bf30bf5439-s.18ql67ww2ii1-.woff2 +0 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/media/6306c77e7c8268e4-s.1ygs37po_4mpd.woff2 +0 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/media/7178b3e590c64307-s.21jp631_3pja2.woff2 +0 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/media/797e433ab948586e-s.p.0w5z4e7s8jfe5.woff2 +0 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/media/7d817b4c03b0c5f1-s.2ojkkrs9oa5rc.woff2 +0 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/media/8a480f0b521d4e75-s.1qq4vpdcun5oj.woff2 +0 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/media/bbc41e54d2fcbd21-s.1_6ayb0k2-vor.woff2 +0 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/media/caa3a2e1cccd8315-s.p.0wgildi0cnwt9.woff2 +0 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/media/favicon.2vob68tjqpejf.ico +0 -0
- humanrpc-0.1.0/humanrpc/static/_next/static/media/fef07dbb0973bf53-s.3p2_lha1f2xer.woff2 +0 -0
- humanrpc-0.1.0/humanrpc/static/_not-found/__next._full.txt +16 -0
- humanrpc-0.1.0/humanrpc/static/_not-found/__next._head.txt +6 -0
- humanrpc-0.1.0/humanrpc/static/_not-found/__next._index.txt +5 -0
- humanrpc-0.1.0/humanrpc/static/_not-found/__next._not-found/__PAGE__.txt +5 -0
- humanrpc-0.1.0/humanrpc/static/_not-found/__next._not-found.txt +5 -0
- humanrpc-0.1.0/humanrpc/static/_not-found/__next._tree.txt +2 -0
- humanrpc-0.1.0/humanrpc/static/_not-found/index.html +1 -0
- humanrpc-0.1.0/humanrpc/static/_not-found/index.txt +16 -0
- humanrpc-0.1.0/humanrpc/static/favicon.ico +0 -0
- humanrpc-0.1.0/humanrpc/static/file.svg +1 -0
- humanrpc-0.1.0/humanrpc/static/globe.svg +1 -0
- humanrpc-0.1.0/humanrpc/static/index.html +1 -0
- humanrpc-0.1.0/humanrpc/static/index.txt +20 -0
- humanrpc-0.1.0/humanrpc/static/next.svg +1 -0
- humanrpc-0.1.0/humanrpc/static/vercel.svg +1 -0
- humanrpc-0.1.0/humanrpc/static/window.svg +1 -0
- humanrpc-0.1.0/pyproject.toml +42 -0
humanrpc-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
|
humanrpc-0.1.0/README.md
ADDED
|
@@ -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,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}
|