hostel-protocol-python 0.1.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.
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""hostel_protocol — Pydantic convenience layer for the Hostel protocol."""
|
|
2
|
+
|
|
3
|
+
from hostel_protocol.converter import proto_to_pydantic, pydantic_to_proto
|
|
4
|
+
from hostel_protocol.models import (
|
|
5
|
+
AIChatMessage,
|
|
6
|
+
ChatMessage,
|
|
7
|
+
ChatRequest,
|
|
8
|
+
ChatResponse,
|
|
9
|
+
CreateComponentRequest,
|
|
10
|
+
CreateComponentResponse,
|
|
11
|
+
CreateTaskRequest,
|
|
12
|
+
CreateTaskResponse,
|
|
13
|
+
DeleteComponentRequest,
|
|
14
|
+
DeleteComponentResponse,
|
|
15
|
+
DeleteTaskRequest,
|
|
16
|
+
DeleteTaskResponse,
|
|
17
|
+
GetComponentRequest,
|
|
18
|
+
GetComponentResponse,
|
|
19
|
+
GetTaskRequest,
|
|
20
|
+
GetTaskResponse,
|
|
21
|
+
HostelMessage,
|
|
22
|
+
HumanChatMessage,
|
|
23
|
+
ListAgentsRequest,
|
|
24
|
+
ListAgentsResponse,
|
|
25
|
+
ListComponentsRequest,
|
|
26
|
+
ListComponentsResponse,
|
|
27
|
+
ListTasksRequest,
|
|
28
|
+
ListTasksResponse,
|
|
29
|
+
TaskData,
|
|
30
|
+
ToolChatMessage,
|
|
31
|
+
UpdateComponentRequest,
|
|
32
|
+
UpdateComponentResponse,
|
|
33
|
+
UpdateTaskRequest,
|
|
34
|
+
UpdateTaskResponse,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
# Converter
|
|
39
|
+
"pydantic_to_proto",
|
|
40
|
+
"proto_to_pydantic",
|
|
41
|
+
# Chat
|
|
42
|
+
"HumanChatMessage",
|
|
43
|
+
"AIChatMessage",
|
|
44
|
+
"ToolChatMessage",
|
|
45
|
+
"ChatMessage",
|
|
46
|
+
"ChatRequest",
|
|
47
|
+
"ChatResponse",
|
|
48
|
+
# Agent
|
|
49
|
+
"ListAgentsRequest",
|
|
50
|
+
"ListAgentsResponse",
|
|
51
|
+
# Component
|
|
52
|
+
"CreateComponentRequest",
|
|
53
|
+
"CreateComponentResponse",
|
|
54
|
+
"GetComponentRequest",
|
|
55
|
+
"GetComponentResponse",
|
|
56
|
+
"ListComponentsRequest",
|
|
57
|
+
"ListComponentsResponse",
|
|
58
|
+
"UpdateComponentRequest",
|
|
59
|
+
"UpdateComponentResponse",
|
|
60
|
+
"DeleteComponentRequest",
|
|
61
|
+
"DeleteComponentResponse",
|
|
62
|
+
# Task
|
|
63
|
+
"TaskData",
|
|
64
|
+
"CreateTaskRequest",
|
|
65
|
+
"CreateTaskResponse",
|
|
66
|
+
"ListTasksRequest",
|
|
67
|
+
"ListTasksResponse",
|
|
68
|
+
"GetTaskRequest",
|
|
69
|
+
"GetTaskResponse",
|
|
70
|
+
"UpdateTaskRequest",
|
|
71
|
+
"UpdateTaskResponse",
|
|
72
|
+
"DeleteTaskRequest",
|
|
73
|
+
"DeleteTaskResponse",
|
|
74
|
+
# Envelope
|
|
75
|
+
"HostelMessage",
|
|
76
|
+
]
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Bidirectional converter between Pydantic models and Protobuf messages."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from google.protobuf import struct_pb2, wrappers_pb2
|
|
8
|
+
from google.protobuf.json_format import MessageToDict, ParseDict
|
|
9
|
+
from google.protobuf.message import Message
|
|
10
|
+
from hostel.protocol.v1 import (
|
|
11
|
+
agent_pb2,
|
|
12
|
+
chat_pb2,
|
|
13
|
+
component_pb2,
|
|
14
|
+
message_pb2,
|
|
15
|
+
task_pb2,
|
|
16
|
+
)
|
|
17
|
+
from pydantic import BaseModel
|
|
18
|
+
|
|
19
|
+
from hostel_protocol import models
|
|
20
|
+
|
|
21
|
+
# ---------------------------------------------------------------------------
|
|
22
|
+
# Registry: Pydantic model class <-> Protobuf message class
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
_PYDANTIC_TO_PROTO: dict[type[BaseModel], type[Message]] = {
|
|
26
|
+
# Chat
|
|
27
|
+
models.HumanChatMessage: chat_pb2.HumanChatMessage,
|
|
28
|
+
models.AIChatMessage: chat_pb2.AIChatMessage,
|
|
29
|
+
models.ToolChatMessage: chat_pb2.ToolChatMessage,
|
|
30
|
+
models.ChatMessage: chat_pb2.ChatMessage,
|
|
31
|
+
models.ChatRequest: chat_pb2.ChatRequest,
|
|
32
|
+
models.ChatResponse: chat_pb2.ChatResponse,
|
|
33
|
+
# Agent
|
|
34
|
+
models.ListAgentsRequest: agent_pb2.ListAgentsRequest,
|
|
35
|
+
models.ListAgentsResponse: agent_pb2.ListAgentsResponse,
|
|
36
|
+
# Component
|
|
37
|
+
models.CreateComponentRequest: component_pb2.CreateComponentRequest,
|
|
38
|
+
models.CreateComponentResponse: component_pb2.CreateComponentResponse,
|
|
39
|
+
models.GetComponentRequest: component_pb2.GetComponentRequest,
|
|
40
|
+
models.GetComponentResponse: component_pb2.GetComponentResponse,
|
|
41
|
+
models.ListComponentsRequest: component_pb2.ListComponentsRequest,
|
|
42
|
+
models.ListComponentsResponse: component_pb2.ListComponentsResponse,
|
|
43
|
+
models.UpdateComponentRequest: component_pb2.UpdateComponentRequest,
|
|
44
|
+
models.UpdateComponentResponse: component_pb2.UpdateComponentResponse,
|
|
45
|
+
models.DeleteComponentRequest: component_pb2.DeleteComponentRequest,
|
|
46
|
+
models.DeleteComponentResponse: component_pb2.DeleteComponentResponse,
|
|
47
|
+
# Task
|
|
48
|
+
models.TaskData: task_pb2.TaskData,
|
|
49
|
+
models.CreateTaskRequest: task_pb2.CreateTaskRequest,
|
|
50
|
+
models.CreateTaskResponse: task_pb2.CreateTaskResponse,
|
|
51
|
+
models.ListTasksRequest: task_pb2.ListTasksRequest,
|
|
52
|
+
models.ListTasksResponse: task_pb2.ListTasksResponse,
|
|
53
|
+
models.GetTaskRequest: task_pb2.GetTaskRequest,
|
|
54
|
+
models.GetTaskResponse: task_pb2.GetTaskResponse,
|
|
55
|
+
models.UpdateTaskRequest: task_pb2.UpdateTaskRequest,
|
|
56
|
+
models.UpdateTaskResponse: task_pb2.UpdateTaskResponse,
|
|
57
|
+
models.DeleteTaskRequest: task_pb2.DeleteTaskRequest,
|
|
58
|
+
models.DeleteTaskResponse: task_pb2.DeleteTaskResponse,
|
|
59
|
+
# Envelope
|
|
60
|
+
models.HostelMessage: message_pb2.HostelMessage,
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
_PROTO_TO_PYDANTIC: dict[type[Message], type[BaseModel]] = {v: k for k, v in _PYDANTIC_TO_PROTO.items()}
|
|
64
|
+
|
|
65
|
+
# Fields in each proto that use google.protobuf.StringValue (nullable strings)
|
|
66
|
+
_STRING_VALUE_FIELDS: dict[type[Message], set[str]] = {
|
|
67
|
+
task_pb2.TaskData: {"webhook_url", "status", "response", "created_at", "updated_at", "executed_at"},
|
|
68
|
+
task_pb2.CreateTaskRequest: {"webhook_url"},
|
|
69
|
+
task_pb2.UpdateTaskRequest: {"agent_name", "prompt", "start_datetime", "webhook_url", "status"},
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
# Fields that hold google.protobuf.Struct (dict[str, Any])
|
|
73
|
+
_STRUCT_FIELDS: dict[type[Message], set[str]] = {
|
|
74
|
+
chat_pb2.ChatResponse: {"content"},
|
|
75
|
+
component_pb2.CreateComponentRequest: {"data"},
|
|
76
|
+
component_pb2.GetComponentResponse: {"data"},
|
|
77
|
+
component_pb2.UpdateComponentRequest: {"data"},
|
|
78
|
+
message_pb2.HostelMessage: {"meta", "system_payload", "generic_payload"},
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
# Fields that hold google.protobuf.Value
|
|
82
|
+
_VALUE_FIELDS: dict[type[Message], set[str]] = {
|
|
83
|
+
chat_pb2.ChatResponse: {"content"},
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
# Fields that hold repeated sub-messages
|
|
87
|
+
_REPEATED_MSG_FIELDS: dict[type[Message], dict[str, type[Message]]] = {
|
|
88
|
+
agent_pb2.ListAgentsResponse: {"agents": struct_pb2.Struct},
|
|
89
|
+
chat_pb2.ChatRequest: {"messages": chat_pb2.ChatMessage},
|
|
90
|
+
component_pb2.ListComponentsResponse: {"components": struct_pb2.Struct},
|
|
91
|
+
task_pb2.ListTasksResponse: {"tasks": task_pb2.TaskData},
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
# Fields that hold a single sub-message (non-oneof)
|
|
95
|
+
_SUB_MSG_FIELDS: dict[type[Message], dict[str, type[Message]]] = {
|
|
96
|
+
task_pb2.CreateTaskResponse: {"task": task_pb2.TaskData},
|
|
97
|
+
task_pb2.GetTaskResponse: {"task": task_pb2.TaskData},
|
|
98
|
+
task_pb2.UpdateTaskResponse: {"task": task_pb2.TaskData},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# oneof groups: proto class -> {field_name: proto sub-message class}
|
|
102
|
+
_ONEOF_FIELDS: dict[type[Message], dict[str, type[Message]]] = {
|
|
103
|
+
chat_pb2.ChatMessage: {
|
|
104
|
+
"human": chat_pb2.HumanChatMessage,
|
|
105
|
+
"ai": chat_pb2.AIChatMessage,
|
|
106
|
+
"tool": chat_pb2.ToolChatMessage,
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
# HostelMessage payload oneof (special handling due to breadth)
|
|
111
|
+
_HOSTEL_MESSAGE_PAYLOAD_FIELDS: dict[str, type[Message]] = {
|
|
112
|
+
"agent_list_request": agent_pb2.ListAgentsRequest,
|
|
113
|
+
"agent_list_response": agent_pb2.ListAgentsResponse,
|
|
114
|
+
"system_payload": struct_pb2.Struct,
|
|
115
|
+
"chat_request": chat_pb2.ChatRequest,
|
|
116
|
+
"chat_response_chunk": chat_pb2.ChatResponse,
|
|
117
|
+
"task_create": task_pb2.CreateTaskRequest,
|
|
118
|
+
"task_create_response": task_pb2.CreateTaskResponse,
|
|
119
|
+
"task_list": task_pb2.ListTasksRequest,
|
|
120
|
+
"task_list_response": task_pb2.ListTasksResponse,
|
|
121
|
+
"task_get": task_pb2.GetTaskRequest,
|
|
122
|
+
"task_get_response": task_pb2.GetTaskResponse,
|
|
123
|
+
"task_update": task_pb2.UpdateTaskRequest,
|
|
124
|
+
"task_update_response": task_pb2.UpdateTaskResponse,
|
|
125
|
+
"task_delete": task_pb2.DeleteTaskRequest,
|
|
126
|
+
"task_delete_response": task_pb2.DeleteTaskResponse,
|
|
127
|
+
"component_create": component_pb2.CreateComponentRequest,
|
|
128
|
+
"component_create_response": component_pb2.CreateComponentResponse,
|
|
129
|
+
"component_get": component_pb2.GetComponentRequest,
|
|
130
|
+
"component_get_response": component_pb2.GetComponentResponse,
|
|
131
|
+
"component_list": component_pb2.ListComponentsRequest,
|
|
132
|
+
"component_list_response": component_pb2.ListComponentsResponse,
|
|
133
|
+
"component_update": component_pb2.UpdateComponentRequest,
|
|
134
|
+
"component_update_response": component_pb2.UpdateComponentResponse,
|
|
135
|
+
"component_delete": component_pb2.DeleteComponentRequest,
|
|
136
|
+
"component_delete_response": component_pb2.DeleteComponentResponse,
|
|
137
|
+
"generic_payload": struct_pb2.Struct,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# Helpers
|
|
143
|
+
# ---------------------------------------------------------------------------
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _struct_to_dict(s: struct_pb2.Struct) -> dict[str, Any]:
|
|
147
|
+
return dict(MessageToDict(s))
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _dict_to_struct(d: dict[str, Any]) -> struct_pb2.Struct:
|
|
151
|
+
s = struct_pb2.Struct()
|
|
152
|
+
s.update(d)
|
|
153
|
+
return s
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _value_to_python(v: struct_pb2.Value) -> Any:
|
|
157
|
+
return MessageToDict(v)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _python_to_value(v: Any) -> struct_pb2.Value:
|
|
161
|
+
val = struct_pb2.Value()
|
|
162
|
+
if v is None:
|
|
163
|
+
val.null_value = 0 # type: ignore[assignment]
|
|
164
|
+
elif isinstance(v, bool):
|
|
165
|
+
val.bool_value = v
|
|
166
|
+
elif isinstance(v, (int, float)):
|
|
167
|
+
val.number_value = float(v)
|
|
168
|
+
elif isinstance(v, str):
|
|
169
|
+
val.string_value = v
|
|
170
|
+
elif isinstance(v, dict):
|
|
171
|
+
ParseDict(v, val.struct_value)
|
|
172
|
+
elif isinstance(v, list):
|
|
173
|
+
for item in v:
|
|
174
|
+
val.list_value.values.append(_python_to_value(item))
|
|
175
|
+
return val
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
# Pydantic → Protobuf
|
|
180
|
+
# ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def pydantic_to_proto(model: BaseModel) -> Message:
|
|
184
|
+
"""Convert a Pydantic model instance to the corresponding Protobuf message."""
|
|
185
|
+
proto_cls = _PYDANTIC_TO_PROTO.get(type(model))
|
|
186
|
+
if proto_cls is None:
|
|
187
|
+
raise TypeError(f"No protobuf mapping registered for {type(model).__name__}")
|
|
188
|
+
return _pydantic_to_proto_inner(model, proto_cls)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def _pydantic_to_proto_inner(model: BaseModel, proto_cls: type[Message]) -> Message:
|
|
192
|
+
proto = proto_cls()
|
|
193
|
+
string_value_fields = _STRING_VALUE_FIELDS.get(proto_cls, set())
|
|
194
|
+
struct_fields = _STRUCT_FIELDS.get(proto_cls, set())
|
|
195
|
+
value_fields = _VALUE_FIELDS.get(proto_cls, set())
|
|
196
|
+
repeated_fields = _REPEATED_MSG_FIELDS.get(proto_cls, {})
|
|
197
|
+
sub_msg_fields = _SUB_MSG_FIELDS.get(proto_cls, {})
|
|
198
|
+
oneof_fields = _ONEOF_FIELDS.get(proto_cls, {})
|
|
199
|
+
|
|
200
|
+
# Special handling for HostelMessage payload oneof
|
|
201
|
+
is_envelope = proto_cls is message_pb2.HostelMessage
|
|
202
|
+
|
|
203
|
+
for field_name, value in model:
|
|
204
|
+
if value is None:
|
|
205
|
+
continue
|
|
206
|
+
|
|
207
|
+
# StringValue wrapper fields
|
|
208
|
+
if field_name in string_value_fields:
|
|
209
|
+
wrapper = wrappers_pb2.StringValue(value=value)
|
|
210
|
+
getattr(proto, field_name).CopyFrom(wrapper)
|
|
211
|
+
|
|
212
|
+
# google.protobuf.Value fields
|
|
213
|
+
elif field_name in value_fields:
|
|
214
|
+
getattr(proto, field_name).CopyFrom(_python_to_value(value))
|
|
215
|
+
|
|
216
|
+
# Struct fields (dict)
|
|
217
|
+
elif field_name in struct_fields:
|
|
218
|
+
if isinstance(value, dict):
|
|
219
|
+
getattr(proto, field_name).CopyFrom(_dict_to_struct(value))
|
|
220
|
+
# repeated Struct handled below
|
|
221
|
+
|
|
222
|
+
# Repeated Struct (list[dict])
|
|
223
|
+
elif field_name in repeated_fields and repeated_fields[field_name] is struct_pb2.Struct:
|
|
224
|
+
for item in value:
|
|
225
|
+
getattr(proto, field_name).append(_dict_to_struct(item))
|
|
226
|
+
|
|
227
|
+
# Repeated sub-messages
|
|
228
|
+
elif field_name in repeated_fields:
|
|
229
|
+
child_proto_cls = repeated_fields[field_name]
|
|
230
|
+
child_pydantic_cls = _PROTO_TO_PYDANTIC.get(child_proto_cls)
|
|
231
|
+
for item in value:
|
|
232
|
+
if child_pydantic_cls and isinstance(item, child_pydantic_cls):
|
|
233
|
+
getattr(proto, field_name).append(_pydantic_to_proto_inner(item, child_proto_cls))
|
|
234
|
+
|
|
235
|
+
# Single sub-message fields
|
|
236
|
+
elif field_name in sub_msg_fields:
|
|
237
|
+
child_proto_cls = sub_msg_fields[field_name]
|
|
238
|
+
getattr(proto, field_name).CopyFrom(_pydantic_to_proto_inner(value, child_proto_cls))
|
|
239
|
+
|
|
240
|
+
# Oneof sub-message fields
|
|
241
|
+
elif field_name in oneof_fields:
|
|
242
|
+
child_proto_cls = oneof_fields[field_name]
|
|
243
|
+
getattr(proto, field_name).CopyFrom(_pydantic_to_proto_inner(value, child_proto_cls))
|
|
244
|
+
|
|
245
|
+
# HostelMessage payload oneof
|
|
246
|
+
elif is_envelope and field_name in _HOSTEL_MESSAGE_PAYLOAD_FIELDS:
|
|
247
|
+
child_proto_cls = _HOSTEL_MESSAGE_PAYLOAD_FIELDS[field_name]
|
|
248
|
+
if child_proto_cls is struct_pb2.Struct:
|
|
249
|
+
getattr(proto, field_name).CopyFrom(_dict_to_struct(value))
|
|
250
|
+
else:
|
|
251
|
+
child_pydantic_cls = _PROTO_TO_PYDANTIC.get(child_proto_cls)
|
|
252
|
+
if child_pydantic_cls and isinstance(value, child_pydantic_cls):
|
|
253
|
+
getattr(proto, field_name).CopyFrom(_pydantic_to_proto_inner(value, child_proto_cls))
|
|
254
|
+
|
|
255
|
+
# Scalar fields
|
|
256
|
+
else:
|
|
257
|
+
setattr(proto, field_name, value)
|
|
258
|
+
|
|
259
|
+
return proto
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# ---------------------------------------------------------------------------
|
|
263
|
+
# Protobuf → Pydantic
|
|
264
|
+
# ---------------------------------------------------------------------------
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def proto_to_pydantic(proto: Message) -> BaseModel:
|
|
268
|
+
"""Convert a Protobuf message to the corresponding Pydantic model."""
|
|
269
|
+
pydantic_cls = _PROTO_TO_PYDANTIC.get(type(proto))
|
|
270
|
+
if pydantic_cls is None:
|
|
271
|
+
raise TypeError(f"No pydantic mapping registered for {type(proto).__name__}")
|
|
272
|
+
return _proto_to_pydantic_inner(proto, pydantic_cls)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _proto_to_pydantic_inner(proto: Message, pydantic_cls: type[BaseModel]) -> BaseModel:
|
|
276
|
+
proto_cls = type(proto)
|
|
277
|
+
string_value_fields = _STRING_VALUE_FIELDS.get(proto_cls, set())
|
|
278
|
+
struct_fields = _STRUCT_FIELDS.get(proto_cls, set())
|
|
279
|
+
value_fields = _VALUE_FIELDS.get(proto_cls, set())
|
|
280
|
+
repeated_fields = _REPEATED_MSG_FIELDS.get(proto_cls, {})
|
|
281
|
+
sub_msg_fields = _SUB_MSG_FIELDS.get(proto_cls, {})
|
|
282
|
+
oneof_fields = _ONEOF_FIELDS.get(proto_cls, {})
|
|
283
|
+
|
|
284
|
+
is_envelope = proto_cls is message_pb2.HostelMessage
|
|
285
|
+
|
|
286
|
+
kwargs: dict[str, Any] = {}
|
|
287
|
+
|
|
288
|
+
for field_desc in proto.DESCRIPTOR.fields:
|
|
289
|
+
field_name = field_desc.name
|
|
290
|
+
|
|
291
|
+
# StringValue wrapper fields
|
|
292
|
+
if field_name in string_value_fields:
|
|
293
|
+
if proto.HasField(field_name):
|
|
294
|
+
kwargs[field_name] = getattr(proto, field_name).value
|
|
295
|
+
else:
|
|
296
|
+
kwargs[field_name] = None
|
|
297
|
+
|
|
298
|
+
# google.protobuf.Value fields
|
|
299
|
+
elif field_name in value_fields:
|
|
300
|
+
if proto.HasField(field_name):
|
|
301
|
+
kwargs[field_name] = _value_to_python(getattr(proto, field_name))
|
|
302
|
+
else:
|
|
303
|
+
kwargs[field_name] = None
|
|
304
|
+
|
|
305
|
+
# Struct fields (dict)
|
|
306
|
+
elif field_name in struct_fields and field_name not in repeated_fields:
|
|
307
|
+
if is_envelope and field_name in {"system_payload", "generic_payload"}:
|
|
308
|
+
# These are oneof payload members
|
|
309
|
+
if proto.HasField(field_name):
|
|
310
|
+
kwargs[field_name] = _struct_to_dict(getattr(proto, field_name))
|
|
311
|
+
else:
|
|
312
|
+
kwargs[field_name] = None
|
|
313
|
+
elif proto.HasField(field_name):
|
|
314
|
+
kwargs[field_name] = _struct_to_dict(getattr(proto, field_name))
|
|
315
|
+
else:
|
|
316
|
+
kwargs[field_name] = {}
|
|
317
|
+
|
|
318
|
+
# Repeated Struct (list[dict])
|
|
319
|
+
elif field_name in repeated_fields and repeated_fields[field_name] is struct_pb2.Struct:
|
|
320
|
+
kwargs[field_name] = [_struct_to_dict(item) for item in getattr(proto, field_name)]
|
|
321
|
+
|
|
322
|
+
# Repeated sub-messages
|
|
323
|
+
elif field_name in repeated_fields:
|
|
324
|
+
child_proto_cls = repeated_fields[field_name]
|
|
325
|
+
child_pydantic_cls = _PROTO_TO_PYDANTIC.get(child_proto_cls)
|
|
326
|
+
if child_pydantic_cls:
|
|
327
|
+
kwargs[field_name] = [
|
|
328
|
+
_proto_to_pydantic_inner(item, child_pydantic_cls) for item in getattr(proto, field_name)
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
# Single sub-message fields
|
|
332
|
+
elif field_name in sub_msg_fields:
|
|
333
|
+
if proto.HasField(field_name):
|
|
334
|
+
child_proto_cls = sub_msg_fields[field_name]
|
|
335
|
+
child_pydantic_cls = _PROTO_TO_PYDANTIC.get(child_proto_cls)
|
|
336
|
+
if child_pydantic_cls:
|
|
337
|
+
kwargs[field_name] = _proto_to_pydantic_inner(getattr(proto, field_name), child_pydantic_cls)
|
|
338
|
+
else:
|
|
339
|
+
kwargs[field_name] = None
|
|
340
|
+
|
|
341
|
+
# Oneof sub-message fields
|
|
342
|
+
elif field_name in oneof_fields:
|
|
343
|
+
if proto.HasField(field_name):
|
|
344
|
+
child_proto_cls = oneof_fields[field_name]
|
|
345
|
+
child_pydantic_cls = _PROTO_TO_PYDANTIC.get(child_proto_cls)
|
|
346
|
+
if child_pydantic_cls:
|
|
347
|
+
kwargs[field_name] = _proto_to_pydantic_inner(getattr(proto, field_name), child_pydantic_cls)
|
|
348
|
+
else:
|
|
349
|
+
kwargs[field_name] = None
|
|
350
|
+
|
|
351
|
+
# HostelMessage payload oneof
|
|
352
|
+
elif is_envelope and field_name in _HOSTEL_MESSAGE_PAYLOAD_FIELDS:
|
|
353
|
+
if proto.HasField(field_name):
|
|
354
|
+
child_proto_cls = _HOSTEL_MESSAGE_PAYLOAD_FIELDS[field_name]
|
|
355
|
+
if child_proto_cls is struct_pb2.Struct:
|
|
356
|
+
kwargs[field_name] = _struct_to_dict(getattr(proto, field_name))
|
|
357
|
+
else:
|
|
358
|
+
child_pydantic_cls = _PROTO_TO_PYDANTIC.get(child_proto_cls)
|
|
359
|
+
if child_pydantic_cls:
|
|
360
|
+
kwargs[field_name] = _proto_to_pydantic_inner(getattr(proto, field_name), child_pydantic_cls)
|
|
361
|
+
else:
|
|
362
|
+
kwargs[field_name] = None
|
|
363
|
+
|
|
364
|
+
# Scalar fields
|
|
365
|
+
else:
|
|
366
|
+
kwargs[field_name] = getattr(proto, field_name)
|
|
367
|
+
|
|
368
|
+
return pydantic_cls(**kwargs)
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
"""Pydantic models mirroring the Hostel protocol protobuf definitions."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
|
8
|
+
|
|
9
|
+
# ---------------------------------------------------------------------------
|
|
10
|
+
# Chat
|
|
11
|
+
# ---------------------------------------------------------------------------
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class HumanChatMessage(BaseModel):
|
|
15
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
16
|
+
content: str
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class AIChatMessage(BaseModel):
|
|
20
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
21
|
+
content: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ToolChatMessage(BaseModel):
|
|
25
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
26
|
+
content: str
|
|
27
|
+
tool_name: str
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ChatMessage(BaseModel):
|
|
31
|
+
"""Wrapper matching the protobuf ``oneof message`` in ``ChatMessage``."""
|
|
32
|
+
|
|
33
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
34
|
+
human: HumanChatMessage | None = None
|
|
35
|
+
ai: AIChatMessage | None = None
|
|
36
|
+
tool: ToolChatMessage | None = None
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class ChatRequest(BaseModel):
|
|
40
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
41
|
+
agent_name: str
|
|
42
|
+
messages: list[ChatMessage] = []
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ChatResponse(BaseModel):
|
|
46
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
47
|
+
role: str = ""
|
|
48
|
+
scope: str = ""
|
|
49
|
+
content: Any = None
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ---------------------------------------------------------------------------
|
|
53
|
+
# Agent
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ListAgentsRequest(BaseModel):
|
|
58
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ListAgentsResponse(BaseModel):
|
|
62
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
63
|
+
agents: list[dict[str, Any]] = []
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# Component
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
class CreateComponentRequest(BaseModel):
|
|
72
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
73
|
+
component_type: str
|
|
74
|
+
data: dict[str, Any] = {}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class CreateComponentResponse(BaseModel):
|
|
78
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
79
|
+
component_type: str = ""
|
|
80
|
+
name: str = ""
|
|
81
|
+
success: bool = False
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class GetComponentRequest(BaseModel):
|
|
85
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
86
|
+
component_type: str
|
|
87
|
+
name: str
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class GetComponentResponse(BaseModel):
|
|
91
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
92
|
+
component_type: str = ""
|
|
93
|
+
data: dict[str, Any] = {}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class ListComponentsRequest(BaseModel):
|
|
97
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
98
|
+
component_type: str
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class ListComponentsResponse(BaseModel):
|
|
102
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
103
|
+
component_type: str = ""
|
|
104
|
+
components: list[dict[str, Any]] = []
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class UpdateComponentRequest(BaseModel):
|
|
108
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
109
|
+
component_type: str
|
|
110
|
+
name: str
|
|
111
|
+
data: dict[str, Any] = {}
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class UpdateComponentResponse(BaseModel):
|
|
115
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
116
|
+
component_type: str = ""
|
|
117
|
+
name: str = ""
|
|
118
|
+
success: bool = False
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class DeleteComponentRequest(BaseModel):
|
|
122
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
123
|
+
component_type: str
|
|
124
|
+
name: str
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class DeleteComponentResponse(BaseModel):
|
|
128
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
129
|
+
component_type: str = ""
|
|
130
|
+
name: str = ""
|
|
131
|
+
success: bool = False
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# Task
|
|
136
|
+
# ---------------------------------------------------------------------------
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class TaskData(BaseModel):
|
|
140
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
141
|
+
id: str = ""
|
|
142
|
+
agent_name: str = ""
|
|
143
|
+
prompt: str = ""
|
|
144
|
+
start_datetime: str = ""
|
|
145
|
+
webhook_url: str | None = None
|
|
146
|
+
status: str | None = None
|
|
147
|
+
response: str | None = None
|
|
148
|
+
created_at: str | None = None
|
|
149
|
+
updated_at: str | None = None
|
|
150
|
+
executed_at: str | None = None
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class CreateTaskRequest(BaseModel):
|
|
154
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
155
|
+
agent_name: str
|
|
156
|
+
prompt: str
|
|
157
|
+
start_datetime: str
|
|
158
|
+
webhook_url: str | None = None
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
class CreateTaskResponse(BaseModel):
|
|
162
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
163
|
+
task: TaskData | None = None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
class ListTasksRequest(BaseModel):
|
|
167
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
class ListTasksResponse(BaseModel):
|
|
171
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
172
|
+
tasks: list[TaskData] = []
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class GetTaskRequest(BaseModel):
|
|
176
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
177
|
+
task_id: str
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class GetTaskResponse(BaseModel):
|
|
181
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
182
|
+
task: TaskData | None = None
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
class UpdateTaskRequest(BaseModel):
|
|
186
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
187
|
+
task_id: str
|
|
188
|
+
agent_name: str | None = None
|
|
189
|
+
prompt: str | None = None
|
|
190
|
+
start_datetime: str | None = None
|
|
191
|
+
webhook_url: str | None = None
|
|
192
|
+
status: str | None = None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class UpdateTaskResponse(BaseModel):
|
|
196
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
197
|
+
task: TaskData | None = None
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
class DeleteTaskRequest(BaseModel):
|
|
201
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
202
|
+
task_id: str
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
class DeleteTaskResponse(BaseModel):
|
|
206
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
207
|
+
success: bool = False
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
# Envelope (HostelMessage)
|
|
212
|
+
# ---------------------------------------------------------------------------
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class HostelMessage(BaseModel):
|
|
216
|
+
"""Unified envelope mirroring ``hostel.protocol.v1.HostelMessage``.
|
|
217
|
+
|
|
218
|
+
The ``oneof payload`` is modelled as a set of optional fields – exactly one
|
|
219
|
+
should be set at a time, matching the protobuf semantics.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
223
|
+
|
|
224
|
+
# Header
|
|
225
|
+
version: str = ""
|
|
226
|
+
id: str = ""
|
|
227
|
+
type: str = ""
|
|
228
|
+
action: str = ""
|
|
229
|
+
meta: dict[str, Any] = {}
|
|
230
|
+
|
|
231
|
+
# oneof payload – Agent
|
|
232
|
+
agent_list_request: ListAgentsRequest | None = None
|
|
233
|
+
agent_list_response: ListAgentsResponse | None = None
|
|
234
|
+
|
|
235
|
+
# oneof payload – System
|
|
236
|
+
system_payload: dict[str, Any] | None = None
|
|
237
|
+
|
|
238
|
+
# oneof payload – Chat
|
|
239
|
+
chat_request: ChatRequest | None = None
|
|
240
|
+
chat_response_chunk: ChatResponse | None = None
|
|
241
|
+
|
|
242
|
+
# oneof payload – Task
|
|
243
|
+
task_create: CreateTaskRequest | None = None
|
|
244
|
+
task_create_response: CreateTaskResponse | None = None
|
|
245
|
+
task_list: ListTasksRequest | None = None
|
|
246
|
+
task_list_response: ListTasksResponse | None = None
|
|
247
|
+
task_get: GetTaskRequest | None = None
|
|
248
|
+
task_get_response: GetTaskResponse | None = None
|
|
249
|
+
task_update: UpdateTaskRequest | None = None
|
|
250
|
+
task_update_response: UpdateTaskResponse | None = None
|
|
251
|
+
task_delete: DeleteTaskRequest | None = None
|
|
252
|
+
task_delete_response: DeleteTaskResponse | None = None
|
|
253
|
+
|
|
254
|
+
# oneof payload – Component
|
|
255
|
+
component_create: CreateComponentRequest | None = None
|
|
256
|
+
component_create_response: CreateComponentResponse | None = None
|
|
257
|
+
component_get: GetComponentRequest | None = None
|
|
258
|
+
component_get_response: GetComponentResponse | None = None
|
|
259
|
+
component_list: ListComponentsRequest | None = None
|
|
260
|
+
component_list_response: ListComponentsResponse | None = None
|
|
261
|
+
component_update: UpdateComponentRequest | None = None
|
|
262
|
+
component_update_response: UpdateComponentResponse | None = None
|
|
263
|
+
component_delete: DeleteComponentRequest | None = None
|
|
264
|
+
component_delete_response: DeleteComponentResponse | None = None
|
|
265
|
+
|
|
266
|
+
# oneof payload – Generic
|
|
267
|
+
generic_payload: dict[str, Any] | None = None
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: hostel-protocol-python
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Pydantic convenience layer for the Hostel protocol
|
|
5
|
+
Requires-Python: >=3.12
|
|
6
|
+
Requires-Dist: hostel-protocol>=0.1.0
|
|
7
|
+
Requires-Dist: protobuf>=7.0
|
|
8
|
+
Requires-Dist: pydantic>=2.5.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: black; extra == 'dev'
|
|
11
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
12
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest-asyncio; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
15
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
16
|
+
Requires-Dist: types-protobuf; extra == 'dev'
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
hostel_protocol/__init__.py,sha256=ctRJ5Y8Yf1hZlm1QXLViBpc_kKaoPCnETb2T2bSHpyM,1795
|
|
2
|
+
hostel_protocol/converter.py,sha256=qwoJE6NFrpcefm8UQdVIFCdvcF99f4lc5aghdIDy4sc,15593
|
|
3
|
+
hostel_protocol/models.py,sha256=AIOuY7Vmz0YVQeZmQNBw9gD3ZFn-3Uc0ZS1dlmbWcmc,7520
|
|
4
|
+
hostel_protocol_python-0.1.0.dist-info/METADATA,sha256=LCwCwkTkp-DScHHMSbX6EXi36KOJ0xUIFvGA9wA8Ym8,549
|
|
5
|
+
hostel_protocol_python-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
6
|
+
hostel_protocol_python-0.1.0.dist-info/RECORD,,
|