hostel-protocol-python 0.3.0__tar.gz → 0.4.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 (20) hide show
  1. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/PKG-INFO +2 -2
  2. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/pyproject.toml +2 -2
  3. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/src/hostel/protocol/converter.py +52 -1
  4. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/src/hostel/protocol/models.py +66 -0
  5. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/.github/workflows/ci.yml +0 -0
  6. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/.github/workflows/publish.yml +0 -0
  7. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/.gitignore +0 -0
  8. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/Makefile +0 -0
  9. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/src/hostel/client/__init__.py +0 -0
  10. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/src/hostel/client/client.py +0 -0
  11. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/src/hostel/protocol/__init__.py +0 -0
  12. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/src/hostel/py.typed +0 -0
  13. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/src/hostel/transport/__init__.py +0 -0
  14. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/src/hostel/transport/transport.py +0 -0
  15. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/src/hostel/transport/zeromq.py +0 -0
  16. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/tests/__init__.py +0 -0
  17. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/tests/test_client.py +0 -0
  18. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/tests/test_converter.py +0 -0
  19. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/tests/test_models.py +0 -0
  20. {hostel_protocol_python-0.3.0 → hostel_protocol_python-0.4.0}/tests/test_transport.py +0 -0
@@ -1,9 +1,9 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hostel-protocol-python
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Pydantic models, transport and client for the Hostel protocol
5
5
  Requires-Python: >=3.12
6
- Requires-Dist: hostel-protocol>=0.3.0
6
+ Requires-Dist: hostel-protocol>=0.4.0
7
7
  Requires-Dist: protobuf<8,>=7.35.0
8
8
  Requires-Dist: pydantic>=2.5.0
9
9
  Provides-Extra: client
@@ -4,11 +4,11 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hostel-protocol-python"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "Pydantic models, transport and client for the Hostel protocol"
9
9
  requires-python = ">=3.12"
10
10
  dependencies = [
11
- "hostel-protocol>=0.3.0",
11
+ "hostel-protocol>=0.4.0",
12
12
  "pydantic>=2.5.0",
13
13
  "protobuf>=7.35.0,<8",
14
14
  ]
@@ -30,6 +30,15 @@ _PYDANTIC_TO_PROTO: dict[type[BaseModel], type[Message]] = {
30
30
  models.ChatMessage: chat_pb2.ChatMessage,
31
31
  models.ChatRequest: chat_pb2.ChatRequest,
32
32
  models.ChatResponse: chat_pb2.ChatResponse,
33
+ # Conversation
34
+ models.ConversationSummary: chat_pb2.ConversationSummary,
35
+ models.ConversationMessage: chat_pb2.ConversationMessage,
36
+ models.ListConversationsRequest: chat_pb2.ListConversationsRequest,
37
+ models.ListConversationsResponse: chat_pb2.ListConversationsResponse,
38
+ models.GetConversationRequest: chat_pb2.GetConversationRequest,
39
+ models.GetConversationResponse: chat_pb2.GetConversationResponse,
40
+ models.DeleteConversationRequest: chat_pb2.DeleteConversationRequest,
41
+ models.DeleteConversationResponse: chat_pb2.DeleteConversationResponse,
33
42
  # Agent
34
43
  models.ListAgentsRequest: agent_pb2.ListAgentsRequest,
35
44
  models.ListAgentsResponse: agent_pb2.ListAgentsResponse,
@@ -62,6 +71,14 @@ _PYDANTIC_TO_PROTO: dict[type[BaseModel], type[Message]] = {
62
71
 
63
72
  _PROTO_TO_PYDANTIC: dict[type[Message], type[BaseModel]] = {v: k for k, v in _PYDANTIC_TO_PROTO.items()}
64
73
 
74
+ # Fields in each proto that are plain strings but should be treated as optional
75
+ # (empty string on the wire maps to None in Pydantic)
76
+ _OPTIONAL_STRING_FIELDS: dict[type[Message], set[str]] = {
77
+ chat_pb2.ChatRequest: {"conversation_id"},
78
+ chat_pb2.ChatResponse: {"conversation_id"},
79
+ chat_pb2.ConversationMessage: {"tool_name", "tool_call_id", "parameters_json"},
80
+ }
81
+
65
82
  # Fields in each proto that use google.protobuf.StringValue (nullable strings)
66
83
  _STRING_VALUE_FIELDS: dict[type[Message], set[str]] = {
67
84
  task_pb2.TaskData: {"webhook_url", "status", "response", "created_at", "updated_at", "executed_at"},
@@ -87,17 +104,28 @@ _VALUE_FIELDS: dict[type[Message], set[str]] = {
87
104
  _REPEATED_MSG_FIELDS: dict[type[Message], dict[str, type[Message]]] = {
88
105
  agent_pb2.ListAgentsResponse: {"agents": struct_pb2.Struct},
89
106
  chat_pb2.ChatRequest: {"messages": chat_pb2.ChatMessage},
107
+ chat_pb2.ListConversationsResponse: {"conversations": chat_pb2.ConversationSummary},
108
+ chat_pb2.GetConversationResponse: {"messages": chat_pb2.ConversationMessage},
90
109
  component_pb2.ListComponentsResponse: {"components": struct_pb2.Struct},
91
110
  task_pb2.ListTasksResponse: {"tasks": task_pb2.TaskData},
92
111
  }
93
112
 
94
113
  # Fields that hold a single sub-message (non-oneof)
95
114
  _SUB_MSG_FIELDS: dict[type[Message], dict[str, type[Message]]] = {
115
+ chat_pb2.GetConversationResponse: {"conversation": chat_pb2.ConversationSummary},
96
116
  task_pb2.CreateTaskResponse: {"task": task_pb2.TaskData},
97
117
  task_pb2.GetTaskResponse: {"task": task_pb2.TaskData},
98
118
  task_pb2.UpdateTaskResponse: {"task": task_pb2.TaskData},
99
119
  }
100
120
 
121
+ # Enum fields: proto class -> {field_name: {int_value: str_value}}
122
+ # Used to convert between proto integer enum values and Pydantic string literals.
123
+ _ENUM_FIELDS: dict[type[Message], dict[str, dict[int, str]]] = {
124
+ chat_pb2.ChatRequest: {
125
+ "planning_mode": {0: "react", 1: "plan_and_execute"},
126
+ },
127
+ }
128
+
101
129
  # oneof groups: proto class -> {field_name: proto sub-message class}
102
130
  _ONEOF_FIELDS: dict[type[Message], dict[str, type[Message]]] = {
103
131
  chat_pb2.ChatMessage: {
@@ -114,6 +142,12 @@ _HOSTEL_MESSAGE_PAYLOAD_FIELDS: dict[str, type[Message]] = {
114
142
  "system_payload": struct_pb2.Struct,
115
143
  "chat_request": chat_pb2.ChatRequest,
116
144
  "chat_response_chunk": chat_pb2.ChatResponse,
145
+ "conversation_list_request": chat_pb2.ListConversationsRequest,
146
+ "conversation_list_response": chat_pb2.ListConversationsResponse,
147
+ "conversation_get_request": chat_pb2.GetConversationRequest,
148
+ "conversation_get_response": chat_pb2.GetConversationResponse,
149
+ "conversation_delete_request": chat_pb2.DeleteConversationRequest,
150
+ "conversation_delete_response": chat_pb2.DeleteConversationResponse,
117
151
  "task_create": task_pb2.CreateTaskRequest,
118
152
  "task_create_response": task_pb2.CreateTaskResponse,
119
153
  "task_list": task_pb2.ListTasksRequest,
@@ -191,11 +225,13 @@ def pydantic_to_proto(model: BaseModel) -> Message:
191
225
  def _pydantic_to_proto_inner(model: BaseModel, proto_cls: type[Message]) -> Message:
192
226
  proto = proto_cls()
193
227
  string_value_fields = _STRING_VALUE_FIELDS.get(proto_cls, set())
228
+ optional_string_fields = _OPTIONAL_STRING_FIELDS.get(proto_cls, set())
194
229
  struct_fields = _STRUCT_FIELDS.get(proto_cls, set())
195
230
  value_fields = _VALUE_FIELDS.get(proto_cls, set())
196
231
  repeated_fields = _REPEATED_MSG_FIELDS.get(proto_cls, {})
197
232
  sub_msg_fields = _SUB_MSG_FIELDS.get(proto_cls, {})
198
233
  oneof_fields = _ONEOF_FIELDS.get(proto_cls, {})
234
+ enum_fields = _ENUM_FIELDS.get(proto_cls, {})
199
235
 
200
236
  # Special handling for HostelMessage payload oneof
201
237
  is_envelope = proto_cls is message_pb2.HostelMessage
@@ -252,6 +288,11 @@ def _pydantic_to_proto_inner(model: BaseModel, proto_cls: type[Message]) -> Mess
252
288
  if child_pydantic_cls and isinstance(value, child_pydantic_cls):
253
289
  getattr(proto, field_name).CopyFrom(_pydantic_to_proto_inner(value, child_proto_cls))
254
290
 
291
+ # Enum fields (string literal -> int)
292
+ elif field_name in enum_fields:
293
+ str_to_int = {v: k for k, v in enum_fields[field_name].items()}
294
+ setattr(proto, field_name, str_to_int[value])
295
+
255
296
  # Scalar fields
256
297
  else:
257
298
  setattr(proto, field_name, value)
@@ -275,11 +316,13 @@ def proto_to_pydantic(proto: Message) -> BaseModel:
275
316
  def _proto_to_pydantic_inner(proto: Message, pydantic_cls: type[BaseModel]) -> BaseModel:
276
317
  proto_cls = type(proto)
277
318
  string_value_fields = _STRING_VALUE_FIELDS.get(proto_cls, set())
319
+ optional_string_fields = _OPTIONAL_STRING_FIELDS.get(proto_cls, set())
278
320
  struct_fields = _STRUCT_FIELDS.get(proto_cls, set())
279
321
  value_fields = _VALUE_FIELDS.get(proto_cls, set())
280
322
  repeated_fields = _REPEATED_MSG_FIELDS.get(proto_cls, {})
281
323
  sub_msg_fields = _SUB_MSG_FIELDS.get(proto_cls, {})
282
324
  oneof_fields = _ONEOF_FIELDS.get(proto_cls, {})
325
+ enum_fields = _ENUM_FIELDS.get(proto_cls, {})
283
326
 
284
327
  is_envelope = proto_cls is message_pb2.HostelMessage
285
328
 
@@ -361,8 +404,16 @@ def _proto_to_pydantic_inner(proto: Message, pydantic_cls: type[BaseModel]) -> B
361
404
  else:
362
405
  kwargs[field_name] = None
363
406
 
407
+ # Enum fields (int -> string literal)
408
+ elif field_name in enum_fields:
409
+ kwargs[field_name] = enum_fields[field_name][getattr(proto, field_name)]
410
+
364
411
  # Scalar fields
365
412
  else:
366
- kwargs[field_name] = getattr(proto, field_name)
413
+ raw = getattr(proto, field_name)
414
+ if field_name in optional_string_fields:
415
+ kwargs[field_name] = raw if raw != "" else None
416
+ else:
417
+ kwargs[field_name] = raw
367
418
 
368
419
  return pydantic_cls(**kwargs)
@@ -42,6 +42,7 @@ class ChatRequest(BaseModel):
42
42
  messages: list[ChatMessage] = []
43
43
  planning_mode: Literal["react", "plan_and_execute"] = "react"
44
44
  require_plan_approval: bool = True
45
+ conversation_id: str | None = None
45
46
 
46
47
 
47
48
  class ChatResponse(BaseModel):
@@ -49,6 +50,7 @@ class ChatResponse(BaseModel):
49
50
  role: str = ""
50
51
  scope: str = ""
51
52
  content: Any = None
53
+ conversation_id: str | None = None
52
54
 
53
55
 
54
56
  # ---------------------------------------------------------------------------
@@ -209,6 +211,62 @@ class DeleteTaskResponse(BaseModel):
209
211
  success: bool = False
210
212
 
211
213
 
214
+ # ---------------------------------------------------------------------------
215
+ # Conversation
216
+ # ---------------------------------------------------------------------------
217
+
218
+
219
+ class ConversationSummary(BaseModel):
220
+ model_config = ConfigDict(populate_by_name=True)
221
+ id: str = ""
222
+ agent_name: str = ""
223
+ created_at: str = ""
224
+ updated_at: str = ""
225
+
226
+
227
+ class ConversationMessage(BaseModel):
228
+ model_config = ConfigDict(populate_by_name=True)
229
+ id: str = ""
230
+ conversation_id: str = ""
231
+ role: Literal["human", "ai", "tool_call", "tool_response"] = "human"
232
+ content: str = ""
233
+ tool_name: str | None = None
234
+ tool_call_id: str | None = None
235
+ parameters_json: str | None = None
236
+ sequence: int = 0
237
+ is_complete: bool = True
238
+ created_at: str = ""
239
+
240
+
241
+ class ListConversationsRequest(BaseModel):
242
+ model_config = ConfigDict(populate_by_name=True)
243
+
244
+
245
+ class ListConversationsResponse(BaseModel):
246
+ model_config = ConfigDict(populate_by_name=True)
247
+ conversations: list[ConversationSummary] = []
248
+
249
+
250
+ class GetConversationRequest(BaseModel):
251
+ model_config = ConfigDict(populate_by_name=True)
252
+ conversation_id: str
253
+
254
+
255
+ class GetConversationResponse(BaseModel):
256
+ model_config = ConfigDict(populate_by_name=True)
257
+ conversation: ConversationSummary | None = None
258
+ messages: list[ConversationMessage] = []
259
+
260
+
261
+ class DeleteConversationRequest(BaseModel):
262
+ model_config = ConfigDict(populate_by_name=True)
263
+ conversation_id: str
264
+
265
+
266
+ class DeleteConversationResponse(BaseModel):
267
+ model_config = ConfigDict(populate_by_name=True)
268
+
269
+
212
270
  # ---------------------------------------------------------------------------
213
271
  # Envelope (HostelMessage)
214
272
  # ---------------------------------------------------------------------------
@@ -241,6 +299,14 @@ class HostelMessage(BaseModel):
241
299
  chat_request: ChatRequest | None = None
242
300
  chat_response_chunk: ChatResponse | None = None
243
301
 
302
+ # oneof payload – Conversation
303
+ conversation_list_request: ListConversationsRequest | None = None
304
+ conversation_list_response: ListConversationsResponse | None = None
305
+ conversation_get_request: GetConversationRequest | None = None
306
+ conversation_get_response: GetConversationResponse | None = None
307
+ conversation_delete_request: DeleteConversationRequest | None = None
308
+ conversation_delete_response: DeleteConversationResponse | None = None
309
+
244
310
  # oneof payload – Task
245
311
  task_create: CreateTaskRequest | None = None
246
312
  task_create_response: CreateTaskResponse | None = None