-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathwebhook_server.py
More file actions
133 lines (116 loc) · 4.45 KB
/
Copy pathwebhook_server.py
File metadata and controls
133 lines (116 loc) · 4.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import hashlib
import json
import os
from http.server import BaseHTTPRequestHandler, HTTPServer
from typing import Any
port = int(os.environ.get("PORT", "3000"))
try:
max_request_body_bytes = int(
os.environ.get("CALLE_WEBHOOK_MAX_BODY_BYTES", "10485760")
)
except ValueError:
raise ValueError(
"CALLE_WEBHOOK_MAX_BODY_BYTES must be a positive integer."
) from None
if max_request_body_bytes <= 0:
raise ValueError("CALLE_WEBHOOK_MAX_BODY_BYTES must be a positive integer.")
max_processed_event_ids = 10_000
processed_event_ids: dict[bytes, None] = {}
class WebhookHandler(BaseHTTPRequestHandler):
def do_POST(self) -> None:
if self.path != "/calle/webhook":
self._send_json(404, {"error": "not_found"})
return
content_lengths = self.headers.get_all("content-length", [])
if (
len(content_lengths) != 1
or not content_lengths[0].isascii()
or not content_lengths[0].isdigit()
):
self.close_connection = True
self._send_json(400, {"error": "invalid_content_length"})
return
try:
content_length = int(content_lengths[0])
except ValueError:
self.close_connection = True
self._send_json(400, {"error": "invalid_content_length"})
return
if content_length > max_request_body_bytes:
self.close_connection = True
self._send_json(413, {"error": "payload_too_large"})
return
raw_body = self.rfile.read(content_length)
try:
parsed: Any = json.loads(raw_body)
except (json.JSONDecodeError, UnicodeDecodeError):
self._send_json(400, {"error": "invalid_json"})
return
if not isinstance(parsed, dict):
self._send_json(400, {"error": "invalid_event"})
return
event: dict[str, Any] = parsed
event_id = self.headers.get("CALL-E-Event-Id")
if not event_id or event.get("id") != event_id:
self._send_json(400, {"error": "invalid_event_id"})
return
event_key = hashlib.sha256(event_id.encode()).digest()
event_type = event.get("type")
call = event.get("data")
if (
not isinstance(event_type, str)
or not isinstance(call, dict)
or not isinstance(call.get("id"), str)
):
self._send_json(400, {"error": "invalid_event"})
return
call_id = call["id"]
if event_key in processed_event_ids:
self._send_json(200, {"received": True, "duplicate": True})
return
# Use durable storage in production and persist the id before side effects.
processed_event_ids[event_key] = None
if len(processed_event_ids) > max_processed_event_ids:
processed_event_ids.pop(next(iter(processed_event_ids)))
if event_type == "call.completed":
print(
"Call completed",
{
"call_id": call_id,
"result": call.get("structured_result"),
"summary": call.get("summary"),
"task_completed": call.get("task_completed"),
"completion_confidence": call.get("completion_confidence"),
"evidence": call.get("evidence"),
},
)
else:
print(
"CALL-E webhook event",
{
"id": event["id"],
"type": event_type,
"call_id": call_id,
},
)
self._send_json(200, {"received": True})
def _send_json(self, status_code: int, body: dict[str, Any]) -> None:
payload = json.dumps(body).encode("utf-8")
self.send_response(status_code)
self.send_header("content-type", "application/json")
self.send_header("content-length", str(len(payload)))
self.end_headers()
self.wfile.write(payload)
def log_message(self, format: str, *args: object) -> None:
return
def main() -> None:
server = HTTPServer(("127.0.0.1", port), WebhookHandler)
print(f"CALL-E webhook server listening on http://localhost:{port}/calle/webhook")
try:
server.serve_forever()
except KeyboardInterrupt:
print("\nStopping CALL-E webhook server.")
finally:
server.server_close()
if __name__ == "__main__":
main()