letta-nightly 0.11.7.dev20251007104119__py3-none-any.whl → 0.12.0.dev20251009104148__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.
- letta/__init__.py +1 -1
- letta/adapters/letta_llm_adapter.py +1 -0
- letta/adapters/letta_llm_request_adapter.py +0 -1
- letta/adapters/letta_llm_stream_adapter.py +7 -2
- letta/adapters/simple_llm_request_adapter.py +88 -0
- letta/adapters/simple_llm_stream_adapter.py +192 -0
- letta/agents/agent_loop.py +6 -0
- letta/agents/ephemeral_summary_agent.py +2 -1
- letta/agents/helpers.py +142 -6
- letta/agents/letta_agent.py +13 -33
- letta/agents/letta_agent_batch.py +2 -4
- letta/agents/letta_agent_v2.py +87 -77
- letta/agents/letta_agent_v3.py +927 -0
- letta/agents/voice_agent.py +2 -6
- letta/constants.py +8 -4
- letta/database_utils.py +161 -0
- letta/errors.py +40 -0
- letta/functions/function_sets/base.py +84 -4
- letta/functions/function_sets/multi_agent.py +0 -3
- letta/functions/schema_generator.py +113 -71
- letta/groups/dynamic_multi_agent.py +3 -2
- letta/groups/helpers.py +1 -2
- letta/groups/round_robin_multi_agent.py +3 -2
- letta/groups/sleeptime_multi_agent.py +3 -2
- letta/groups/sleeptime_multi_agent_v2.py +1 -1
- letta/groups/sleeptime_multi_agent_v3.py +17 -17
- letta/groups/supervisor_multi_agent.py +84 -80
- letta/helpers/converters.py +3 -0
- letta/helpers/message_helper.py +4 -0
- letta/helpers/tool_rule_solver.py +92 -5
- letta/interfaces/anthropic_streaming_interface.py +409 -0
- letta/interfaces/gemini_streaming_interface.py +296 -0
- letta/interfaces/openai_streaming_interface.py +752 -1
- letta/llm_api/anthropic_client.py +127 -16
- letta/llm_api/bedrock_client.py +4 -2
- letta/llm_api/deepseek_client.py +4 -1
- letta/llm_api/google_vertex_client.py +124 -42
- letta/llm_api/groq_client.py +4 -1
- letta/llm_api/llm_api_tools.py +11 -4
- letta/llm_api/llm_client_base.py +6 -2
- letta/llm_api/openai.py +32 -2
- letta/llm_api/openai_client.py +423 -18
- letta/llm_api/xai_client.py +4 -1
- letta/main.py +9 -5
- letta/memory.py +1 -0
- letta/orm/__init__.py +2 -1
- letta/orm/agent.py +10 -0
- letta/orm/block.py +7 -16
- letta/orm/blocks_agents.py +8 -2
- letta/orm/files_agents.py +2 -0
- letta/orm/job.py +7 -5
- letta/orm/mcp_oauth.py +1 -0
- letta/orm/message.py +21 -6
- letta/orm/organization.py +2 -0
- letta/orm/provider.py +6 -2
- letta/orm/run.py +71 -0
- letta/orm/run_metrics.py +82 -0
- letta/orm/sandbox_config.py +7 -1
- letta/orm/sqlalchemy_base.py +0 -306
- letta/orm/step.py +6 -5
- letta/orm/step_metrics.py +5 -5
- letta/otel/tracing.py +28 -3
- letta/plugins/defaults.py +4 -4
- letta/prompts/system_prompts/__init__.py +2 -0
- letta/prompts/system_prompts/letta_v1.py +25 -0
- letta/schemas/agent.py +3 -2
- letta/schemas/agent_file.py +9 -3
- letta/schemas/block.py +23 -10
- letta/schemas/enums.py +21 -2
- letta/schemas/job.py +17 -4
- letta/schemas/letta_message_content.py +71 -2
- letta/schemas/letta_stop_reason.py +5 -5
- letta/schemas/llm_config.py +53 -3
- letta/schemas/memory.py +1 -1
- letta/schemas/message.py +564 -117
- letta/schemas/openai/responses_request.py +64 -0
- letta/schemas/providers/__init__.py +2 -0
- letta/schemas/providers/anthropic.py +16 -0
- letta/schemas/providers/ollama.py +115 -33
- letta/schemas/providers/openrouter.py +52 -0
- letta/schemas/providers/vllm.py +2 -1
- letta/schemas/run.py +48 -42
- letta/schemas/run_metrics.py +21 -0
- letta/schemas/step.py +2 -2
- letta/schemas/step_metrics.py +1 -1
- letta/schemas/tool.py +15 -107
- letta/schemas/tool_rule.py +88 -5
- letta/serialize_schemas/marshmallow_agent.py +1 -0
- letta/server/db.py +79 -408
- letta/server/rest_api/app.py +61 -10
- letta/server/rest_api/dependencies.py +14 -0
- letta/server/rest_api/redis_stream_manager.py +19 -8
- letta/server/rest_api/routers/v1/agents.py +364 -292
- letta/server/rest_api/routers/v1/blocks.py +14 -20
- letta/server/rest_api/routers/v1/identities.py +45 -110
- letta/server/rest_api/routers/v1/internal_templates.py +21 -0
- letta/server/rest_api/routers/v1/jobs.py +23 -6
- letta/server/rest_api/routers/v1/messages.py +1 -1
- letta/server/rest_api/routers/v1/runs.py +149 -99
- letta/server/rest_api/routers/v1/sandbox_configs.py +10 -19
- letta/server/rest_api/routers/v1/tools.py +281 -594
- letta/server/rest_api/routers/v1/voice.py +1 -1
- letta/server/rest_api/streaming_response.py +29 -29
- letta/server/rest_api/utils.py +122 -64
- letta/server/server.py +160 -887
- letta/services/agent_manager.py +236 -919
- letta/services/agent_serialization_manager.py +16 -0
- letta/services/archive_manager.py +0 -100
- letta/services/block_manager.py +211 -168
- letta/services/context_window_calculator/token_counter.py +1 -1
- letta/services/file_manager.py +1 -1
- letta/services/files_agents_manager.py +24 -33
- letta/services/group_manager.py +0 -142
- letta/services/helpers/agent_manager_helper.py +7 -2
- letta/services/helpers/run_manager_helper.py +69 -0
- letta/services/job_manager.py +96 -411
- letta/services/lettuce/__init__.py +6 -0
- letta/services/lettuce/lettuce_client_base.py +86 -0
- letta/services/mcp_manager.py +38 -6
- letta/services/message_manager.py +165 -362
- letta/services/organization_manager.py +0 -36
- letta/services/passage_manager.py +0 -345
- letta/services/provider_manager.py +0 -80
- letta/services/run_manager.py +364 -0
- letta/services/sandbox_config_manager.py +0 -234
- letta/services/step_manager.py +62 -39
- letta/services/summarizer/summarizer.py +9 -7
- letta/services/telemetry_manager.py +0 -16
- letta/services/tool_executor/builtin_tool_executor.py +35 -0
- letta/services/tool_executor/core_tool_executor.py +397 -2
- letta/services/tool_executor/files_tool_executor.py +3 -3
- letta/services/tool_executor/multi_agent_tool_executor.py +30 -15
- letta/services/tool_executor/tool_execution_manager.py +6 -8
- letta/services/tool_executor/tool_executor_base.py +3 -3
- letta/services/tool_manager.py +85 -339
- letta/services/tool_sandbox/base.py +24 -13
- letta/services/tool_sandbox/e2b_sandbox.py +16 -1
- letta/services/tool_schema_generator.py +123 -0
- letta/services/user_manager.py +0 -99
- letta/settings.py +20 -4
- letta/system.py +5 -1
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/METADATA +3 -5
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/RECORD +146 -135
- letta/agents/temporal/activities/__init__.py +0 -4
- letta/agents/temporal/activities/example_activity.py +0 -7
- letta/agents/temporal/activities/prepare_messages.py +0 -10
- letta/agents/temporal/temporal_agent_workflow.py +0 -56
- letta/agents/temporal/types.py +0 -25
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/WHEEL +0 -0
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/entry_points.txt +0 -0
- {letta_nightly-0.11.7.dev20251007104119.dist-info → letta_nightly-0.12.0.dev20251009104148.dist-info}/licenses/LICENSE +0 -0
letta/schemas/message.py
CHANGED
@@ -11,9 +11,10 @@ from enum import Enum
|
|
11
11
|
from typing import Annotated, Any, Dict, List, Literal, Optional, Union
|
12
12
|
|
13
13
|
from openai.types.chat.chat_completion_message_tool_call import ChatCompletionMessageToolCall as OpenAIToolCall, Function as OpenAIFunction
|
14
|
+
from openai.types.responses import ResponseReasoningItem
|
14
15
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
15
16
|
|
16
|
-
from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG, TOOL_CALL_ID_MAX_LEN
|
17
|
+
from letta.constants import DEFAULT_MESSAGE_TOOL, DEFAULT_MESSAGE_TOOL_KWARG, REQUEST_HEARTBEAT_PARAM, TOOL_CALL_ID_MAX_LEN
|
17
18
|
from letta.helpers.datetime_helpers import get_utc_time, is_utc_datetime
|
18
19
|
from letta.helpers.json_helpers import json_dumps
|
19
20
|
from letta.local_llm.constants import INNER_THOUGHTS_KWARG, INNER_THOUGHTS_KWARG_VERTEX
|
@@ -25,6 +26,7 @@ from letta.schemas.letta_message import (
|
|
25
26
|
AssistantMessage,
|
26
27
|
HiddenReasoningMessage,
|
27
28
|
LettaMessage,
|
29
|
+
MessageType,
|
28
30
|
ReasoningMessage,
|
29
31
|
SystemMessage,
|
30
32
|
ToolCall,
|
@@ -38,7 +40,9 @@ from letta.schemas.letta_message_content import (
|
|
38
40
|
OmittedReasoningContent,
|
39
41
|
ReasoningContent,
|
40
42
|
RedactedReasoningContent,
|
43
|
+
SummarizedReasoningContent,
|
41
44
|
TextContent,
|
45
|
+
ToolCallContent,
|
42
46
|
ToolReturnContent,
|
43
47
|
get_letta_message_content_union_str_json_schema,
|
44
48
|
)
|
@@ -192,6 +196,7 @@ class Message(BaseMessage):
|
|
192
196
|
tool_call_id: Optional[str] = Field(default=None, description="The ID of the tool call. Only applicable for role tool.")
|
193
197
|
# Extras
|
194
198
|
step_id: Optional[str] = Field(default=None, description="The id of the step that this message was created in.")
|
199
|
+
run_id: Optional[str] = Field(default=None, description="The id of the run that this message was created in.")
|
195
200
|
otid: Optional[str] = Field(default=None, description="The offline threading id associated with this message")
|
196
201
|
tool_returns: Optional[List[ToolReturn]] = Field(default=None, description="Tool execution return information for prior tool calls")
|
197
202
|
group_id: Optional[str] = Field(default=None, description="The multi-agent group that the message was sent in")
|
@@ -208,6 +213,13 @@ class Message(BaseMessage):
|
|
208
213
|
# This overrides the optional base orm schema, created_at MUST exist on all messages objects
|
209
214
|
created_at: datetime = Field(default_factory=get_utc_time, description="The timestamp when the object was created.")
|
210
215
|
|
216
|
+
# validate that run_id is set
|
217
|
+
# @model_validator(mode="after")
|
218
|
+
# def validate_run_id(self):
|
219
|
+
# if self.run_id is None:
|
220
|
+
# raise ValueError("Run ID is required")
|
221
|
+
# return self
|
222
|
+
|
211
223
|
@field_validator("role")
|
212
224
|
@classmethod
|
213
225
|
def validate_role(cls, v: str) -> str:
|
@@ -239,6 +251,7 @@ class Message(BaseMessage):
|
|
239
251
|
assistant_message_tool_kwarg: str = DEFAULT_MESSAGE_TOOL_KWARG,
|
240
252
|
reverse: bool = True,
|
241
253
|
include_err: Optional[bool] = None,
|
254
|
+
text_is_assistant_message: bool = False,
|
242
255
|
) -> List[LettaMessage]:
|
243
256
|
if use_assistant_message:
|
244
257
|
message_ids_to_remove = []
|
@@ -270,6 +283,7 @@ class Message(BaseMessage):
|
|
270
283
|
assistant_message_tool_kwarg=assistant_message_tool_kwarg,
|
271
284
|
reverse=reverse,
|
272
285
|
include_err=include_err,
|
286
|
+
text_is_assistant_message=text_is_assistant_message,
|
273
287
|
)
|
274
288
|
]
|
275
289
|
|
@@ -280,12 +294,15 @@ class Message(BaseMessage):
|
|
280
294
|
assistant_message_tool_kwarg: str = DEFAULT_MESSAGE_TOOL_KWARG,
|
281
295
|
reverse: bool = True,
|
282
296
|
include_err: Optional[bool] = None,
|
297
|
+
text_is_assistant_message: bool = False,
|
283
298
|
) -> List[LettaMessage]:
|
284
299
|
"""Convert message object (in DB format) to the style used by the original Letta API"""
|
300
|
+
|
285
301
|
messages = []
|
286
302
|
if self.role == MessageRole.assistant:
|
287
303
|
if self.content:
|
288
|
-
messages.extend(self._convert_reasoning_messages())
|
304
|
+
messages.extend(self._convert_reasoning_messages(text_is_assistant_message=text_is_assistant_message))
|
305
|
+
|
289
306
|
if self.tool_calls is not None:
|
290
307
|
messages.extend(
|
291
308
|
self._convert_tool_call_messages(
|
@@ -296,14 +313,14 @@ class Message(BaseMessage):
|
|
296
313
|
),
|
297
314
|
)
|
298
315
|
elif self.role == MessageRole.tool:
|
299
|
-
messages.
|
316
|
+
messages.extend(self._convert_tool_return_message())
|
300
317
|
elif self.role == MessageRole.user:
|
301
318
|
messages.append(self._convert_user_message())
|
302
319
|
elif self.role == MessageRole.system:
|
303
320
|
messages.append(self._convert_system_message())
|
304
321
|
elif self.role == MessageRole.approval:
|
305
322
|
if self.content:
|
306
|
-
messages.extend(self._convert_reasoning_messages())
|
323
|
+
messages.extend(self._convert_reasoning_messages(text_is_assistant_message=text_is_assistant_message))
|
307
324
|
if self.tool_calls is not None:
|
308
325
|
tool_calls = self._convert_tool_call_messages()
|
309
326
|
assert len(tool_calls) == 1
|
@@ -317,6 +334,7 @@ class Message(BaseMessage):
|
|
317
334
|
approve=self.approve,
|
318
335
|
approval_request_id=self.approval_request_id,
|
319
336
|
reason=self.denial_reason,
|
337
|
+
run_id=self.run_id,
|
320
338
|
)
|
321
339
|
messages.append(approval_response_message)
|
322
340
|
else:
|
@@ -324,30 +342,37 @@ class Message(BaseMessage):
|
|
324
342
|
|
325
343
|
return messages[::-1] if reverse else messages
|
326
344
|
|
327
|
-
def _convert_reasoning_messages(
|
345
|
+
def _convert_reasoning_messages(
|
346
|
+
self,
|
347
|
+
current_message_count: int = 0,
|
348
|
+
text_is_assistant_message: bool = False, # For v3 loop, set to True
|
349
|
+
) -> List[LettaMessage]:
|
328
350
|
messages = []
|
329
|
-
|
330
|
-
|
351
|
+
|
352
|
+
for content_part in self.content:
|
331
353
|
otid = Message.generate_otid_from_id(self.id, current_message_count + len(messages))
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
|
354
|
+
|
355
|
+
if isinstance(content_part, TextContent):
|
356
|
+
if text_is_assistant_message:
|
357
|
+
# .content is assistant message
|
358
|
+
if messages and messages[-1].message_type == MessageType.assistant_message:
|
359
|
+
messages[-1].content += content_part.text
|
360
|
+
else:
|
361
|
+
messages.append(
|
362
|
+
AssistantMessage(
|
363
|
+
id=self.id,
|
364
|
+
date=self.created_at,
|
365
|
+
content=content_part.text,
|
366
|
+
name=self.name,
|
367
|
+
otid=otid,
|
368
|
+
sender_id=self.sender_id,
|
369
|
+
step_id=self.step_id,
|
370
|
+
is_err=self.is_err,
|
371
|
+
run_id=self.run_id,
|
372
|
+
)
|
373
|
+
)
|
374
|
+
else:
|
375
|
+
# .content is COT
|
351
376
|
messages.append(
|
352
377
|
ReasoningMessage(
|
353
378
|
id=self.id,
|
@@ -358,10 +383,15 @@ class Message(BaseMessage):
|
|
358
383
|
sender_id=self.sender_id,
|
359
384
|
step_id=self.step_id,
|
360
385
|
is_err=self.is_err,
|
386
|
+
run_id=self.run_id,
|
361
387
|
)
|
362
388
|
)
|
363
|
-
|
364
|
-
|
389
|
+
|
390
|
+
elif isinstance(content_part, ReasoningContent):
|
391
|
+
# "native" COT
|
392
|
+
if messages and messages[-1].message_type == MessageType.reasoning_message:
|
393
|
+
messages[-1].reasoning += content_part.reasoning
|
394
|
+
else:
|
365
395
|
messages.append(
|
366
396
|
ReasoningMessage(
|
367
397
|
id=self.id,
|
@@ -373,41 +403,87 @@ class Message(BaseMessage):
|
|
373
403
|
otid=otid,
|
374
404
|
step_id=self.step_id,
|
375
405
|
is_err=self.is_err,
|
406
|
+
run_id=self.run_id,
|
376
407
|
)
|
377
408
|
)
|
378
|
-
|
379
|
-
|
409
|
+
|
410
|
+
elif isinstance(content_part, SummarizedReasoningContent):
|
411
|
+
# TODO remove the cast and just return the native type
|
412
|
+
casted_content_part = content_part.to_reasoning_content()
|
413
|
+
if casted_content_part is not None:
|
380
414
|
messages.append(
|
381
|
-
|
415
|
+
ReasoningMessage(
|
382
416
|
id=self.id,
|
383
417
|
date=self.created_at,
|
384
|
-
|
385
|
-
|
418
|
+
reasoning=casted_content_part.reasoning,
|
419
|
+
source="reasoner_model", # TODO do we want to tag like this?
|
420
|
+
signature=casted_content_part.signature,
|
386
421
|
name=self.name,
|
387
422
|
otid=otid,
|
388
|
-
sender_id=self.sender_id,
|
389
423
|
step_id=self.step_id,
|
390
424
|
is_err=self.is_err,
|
425
|
+
run_id=self.run_id,
|
391
426
|
)
|
392
427
|
)
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
400
|
-
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
428
|
+
|
429
|
+
elif isinstance(content_part, RedactedReasoningContent):
|
430
|
+
# "native" redacted/hidden COT
|
431
|
+
messages.append(
|
432
|
+
HiddenReasoningMessage(
|
433
|
+
id=self.id,
|
434
|
+
date=self.created_at,
|
435
|
+
state="redacted",
|
436
|
+
hidden_reasoning=content_part.data,
|
437
|
+
name=self.name,
|
438
|
+
otid=otid,
|
439
|
+
sender_id=self.sender_id,
|
440
|
+
step_id=self.step_id,
|
441
|
+
is_err=self.is_err,
|
442
|
+
run_id=self.run_id,
|
406
443
|
)
|
407
|
-
|
408
|
-
|
444
|
+
)
|
445
|
+
|
446
|
+
elif isinstance(content_part, OmittedReasoningContent):
|
447
|
+
# Special case for "hidden reasoning" models like o1/o3
|
448
|
+
# NOTE: we also have to think about how to return this during streaming
|
449
|
+
messages.append(
|
450
|
+
HiddenReasoningMessage(
|
451
|
+
id=self.id,
|
452
|
+
date=self.created_at,
|
453
|
+
state="omitted",
|
454
|
+
name=self.name,
|
455
|
+
otid=otid,
|
456
|
+
step_id=self.step_id,
|
457
|
+
is_err=self.is_err,
|
458
|
+
run_id=self.run_id,
|
459
|
+
)
|
460
|
+
)
|
461
|
+
|
462
|
+
else:
|
463
|
+
warnings.warn(f"Unrecognized content part in assistant message: {content_part}")
|
464
|
+
|
409
465
|
return messages
|
410
466
|
|
467
|
+
def _convert_assistant_message(
|
468
|
+
self,
|
469
|
+
) -> AssistantMessage:
|
470
|
+
if self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent):
|
471
|
+
text_content = self.content[0].text
|
472
|
+
else:
|
473
|
+
raise ValueError(f"Invalid assistant message (no text object on message): {self.content}")
|
474
|
+
|
475
|
+
return AssistantMessage(
|
476
|
+
id=self.id,
|
477
|
+
date=self.created_at,
|
478
|
+
content=text_content,
|
479
|
+
name=self.name,
|
480
|
+
otid=self.otid,
|
481
|
+
sender_id=self.sender_id,
|
482
|
+
step_id=self.step_id,
|
483
|
+
# is_err=self.is_err,
|
484
|
+
run_id=self.run_id,
|
485
|
+
)
|
486
|
+
|
411
487
|
def _convert_tool_call_messages(
|
412
488
|
self,
|
413
489
|
current_message_count: int = 0,
|
@@ -438,6 +514,7 @@ class Message(BaseMessage):
|
|
438
514
|
sender_id=self.sender_id,
|
439
515
|
step_id=self.step_id,
|
440
516
|
is_err=self.is_err,
|
517
|
+
run_id=self.run_id,
|
441
518
|
)
|
442
519
|
)
|
443
520
|
else:
|
@@ -455,49 +532,135 @@ class Message(BaseMessage):
|
|
455
532
|
sender_id=self.sender_id,
|
456
533
|
step_id=self.step_id,
|
457
534
|
is_err=self.is_err,
|
535
|
+
run_id=self.run_id,
|
458
536
|
)
|
459
537
|
)
|
460
538
|
return messages
|
461
539
|
|
462
|
-
def _convert_tool_return_message(self) -> ToolReturnMessage:
|
463
|
-
"""Convert tool role message to ToolReturnMessage
|
540
|
+
def _convert_tool_return_message(self) -> List[ToolReturnMessage]:
|
541
|
+
"""Convert tool role message to ToolReturnMessage.
|
464
542
|
|
465
|
-
|
543
|
+
The tool return is packaged as follows:
|
466
544
|
packaged_message = {
|
467
545
|
"status": "OK" if was_success else "Failed",
|
468
546
|
"message": response_string,
|
469
547
|
"time": formatted_time,
|
470
548
|
}
|
549
|
+
|
550
|
+
Returns:
|
551
|
+
List[ToolReturnMessage]: Converted tool return messages
|
552
|
+
|
553
|
+
Raises:
|
554
|
+
ValueError: If message role is not 'tool', parsing fails, or no valid content exists
|
471
555
|
"""
|
472
|
-
if self.
|
473
|
-
|
474
|
-
|
475
|
-
|
556
|
+
if self.role != MessageRole.tool:
|
557
|
+
raise ValueError(f"Cannot convert message of type {self.role} to ToolReturnMessage")
|
558
|
+
|
559
|
+
if self.tool_returns:
|
560
|
+
return self._convert_explicit_tool_returns()
|
561
|
+
|
562
|
+
return self._convert_legacy_tool_return()
|
476
563
|
|
564
|
+
def _convert_explicit_tool_returns(self) -> List[ToolReturnMessage]:
|
565
|
+
"""Convert explicit tool returns to ToolReturnMessage list."""
|
566
|
+
tool_returns = []
|
567
|
+
|
568
|
+
for index, tool_return in enumerate(self.tool_returns):
|
569
|
+
parsed_data = self._parse_tool_response(tool_return.func_response)
|
570
|
+
|
571
|
+
tool_returns.append(
|
572
|
+
self._create_tool_return_message(
|
573
|
+
message_text=parsed_data["message"],
|
574
|
+
status=parsed_data["status"],
|
575
|
+
tool_call_id=tool_return.tool_call_id,
|
576
|
+
stdout=tool_return.stdout,
|
577
|
+
stderr=tool_return.stderr,
|
578
|
+
otid_index=index,
|
579
|
+
)
|
580
|
+
)
|
581
|
+
|
582
|
+
return tool_returns
|
583
|
+
|
584
|
+
def _convert_legacy_tool_return(self) -> List[ToolReturnMessage]:
|
585
|
+
"""Convert legacy single text content to ToolReturnMessage."""
|
586
|
+
if not self._has_single_text_content():
|
587
|
+
raise ValueError(f"No valid tool returns to convert: {self}")
|
588
|
+
|
589
|
+
text_content = self.content[0].text
|
590
|
+
parsed_data = self._parse_tool_response(text_content)
|
591
|
+
|
592
|
+
return [
|
593
|
+
self._create_tool_return_message(
|
594
|
+
message_text=parsed_data["message"],
|
595
|
+
status=parsed_data["status"],
|
596
|
+
tool_call_id=self.tool_call_id,
|
597
|
+
stdout=None,
|
598
|
+
stderr=None,
|
599
|
+
otid_index=0,
|
600
|
+
)
|
601
|
+
]
|
602
|
+
|
603
|
+
def _has_single_text_content(self) -> bool:
|
604
|
+
"""Check if message has exactly one text content item."""
|
605
|
+
return self.content and len(self.content) == 1 and isinstance(self.content[0], TextContent)
|
606
|
+
|
607
|
+
def _parse_tool_response(self, response_text: str) -> dict:
|
608
|
+
"""Parse tool response JSON and extract message and status.
|
609
|
+
|
610
|
+
Args:
|
611
|
+
response_text: Raw JSON response text
|
612
|
+
|
613
|
+
Returns:
|
614
|
+
Dictionary with 'message' and 'status' keys
|
615
|
+
|
616
|
+
Raises:
|
617
|
+
ValueError: If JSON parsing fails
|
618
|
+
"""
|
477
619
|
try:
|
478
|
-
function_return = parse_json(
|
479
|
-
|
480
|
-
|
481
|
-
|
482
|
-
|
620
|
+
function_return = parse_json(response_text)
|
621
|
+
return {
|
622
|
+
"message": str(function_return.get("message", response_text)),
|
623
|
+
"status": self._parse_tool_status(function_return.get("status", "OK")),
|
624
|
+
}
|
625
|
+
except json.JSONDecodeError as e:
|
626
|
+
raise ValueError(f"Failed to decode function return: {response_text}") from e
|
483
627
|
|
484
|
-
|
485
|
-
|
486
|
-
|
628
|
+
def _create_tool_return_message(
|
629
|
+
self,
|
630
|
+
message_text: str,
|
631
|
+
status: str,
|
632
|
+
tool_call_id: Optional[str],
|
633
|
+
stdout: Optional[str],
|
634
|
+
stderr: Optional[str],
|
635
|
+
otid_index: int,
|
636
|
+
) -> ToolReturnMessage:
|
637
|
+
"""Create a ToolReturnMessage with common attributes.
|
487
638
|
|
639
|
+
Args:
|
640
|
+
message_text: The tool return message text
|
641
|
+
status: Tool execution status
|
642
|
+
tool_call_id: Optional tool call identifier
|
643
|
+
stdout: Optional standard output
|
644
|
+
stderr: Optional standard error
|
645
|
+
otid_index: Index for OTID generation
|
646
|
+
|
647
|
+
Returns:
|
648
|
+
Configured ToolReturnMessage instance
|
649
|
+
"""
|
488
650
|
return ToolReturnMessage(
|
489
651
|
id=self.id,
|
490
652
|
date=self.created_at,
|
491
653
|
tool_return=message_text,
|
492
|
-
status=
|
493
|
-
tool_call_id=
|
494
|
-
stdout=
|
495
|
-
stderr=
|
654
|
+
status=status,
|
655
|
+
tool_call_id=tool_call_id,
|
656
|
+
stdout=stdout,
|
657
|
+
stderr=stderr,
|
496
658
|
name=self.name,
|
497
|
-
otid=Message.generate_otid_from_id(self.id,
|
659
|
+
otid=Message.generate_otid_from_id(self.id, otid_index),
|
498
660
|
sender_id=self.sender_id,
|
499
661
|
step_id=self.step_id,
|
500
662
|
is_err=self.is_err,
|
663
|
+
run_id=self.run_id,
|
501
664
|
)
|
502
665
|
|
503
666
|
@staticmethod
|
@@ -531,6 +694,7 @@ class Message(BaseMessage):
|
|
531
694
|
sender_id=self.sender_id,
|
532
695
|
step_id=self.step_id,
|
533
696
|
is_err=self.is_err,
|
697
|
+
run_id=self.run_id,
|
534
698
|
)
|
535
699
|
|
536
700
|
def _convert_system_message(self) -> SystemMessage:
|
@@ -548,6 +712,7 @@ class Message(BaseMessage):
|
|
548
712
|
otid=self.otid,
|
549
713
|
sender_id=self.sender_id,
|
550
714
|
step_id=self.step_id,
|
715
|
+
run_id=self.run_id,
|
551
716
|
)
|
552
717
|
|
553
718
|
@staticmethod
|
@@ -561,6 +726,7 @@ class Message(BaseMessage):
|
|
561
726
|
name: Optional[str] = None,
|
562
727
|
group_id: Optional[str] = None,
|
563
728
|
tool_returns: Optional[List[ToolReturn]] = None,
|
729
|
+
run_id: Optional[str] = None,
|
564
730
|
) -> Message:
|
565
731
|
"""Convert a ChatCompletion message object into a Message object (synced to DB)"""
|
566
732
|
if not created_at:
|
@@ -622,6 +788,7 @@ class Message(BaseMessage):
|
|
622
788
|
id=str(id),
|
623
789
|
tool_returns=tool_returns,
|
624
790
|
group_id=group_id,
|
791
|
+
run_id=run_id,
|
625
792
|
)
|
626
793
|
else:
|
627
794
|
return Message(
|
@@ -636,6 +803,7 @@ class Message(BaseMessage):
|
|
636
803
|
created_at=created_at,
|
637
804
|
tool_returns=tool_returns,
|
638
805
|
group_id=group_id,
|
806
|
+
run_id=run_id,
|
639
807
|
)
|
640
808
|
|
641
809
|
elif "function_call" in openai_message_dict and openai_message_dict["function_call"] is not None:
|
@@ -671,6 +839,7 @@ class Message(BaseMessage):
|
|
671
839
|
id=str(id),
|
672
840
|
tool_returns=tool_returns,
|
673
841
|
group_id=group_id,
|
842
|
+
run_id=run_id,
|
674
843
|
)
|
675
844
|
else:
|
676
845
|
return Message(
|
@@ -685,6 +854,7 @@ class Message(BaseMessage):
|
|
685
854
|
created_at=created_at,
|
686
855
|
tool_returns=tool_returns,
|
687
856
|
group_id=group_id,
|
857
|
+
run_id=run_id,
|
688
858
|
)
|
689
859
|
|
690
860
|
else:
|
@@ -720,6 +890,7 @@ class Message(BaseMessage):
|
|
720
890
|
id=str(id),
|
721
891
|
tool_returns=tool_returns,
|
722
892
|
group_id=group_id,
|
893
|
+
run_id=run_id,
|
723
894
|
)
|
724
895
|
else:
|
725
896
|
return Message(
|
@@ -734,6 +905,7 @@ class Message(BaseMessage):
|
|
734
905
|
created_at=created_at,
|
735
906
|
tool_returns=tool_returns,
|
736
907
|
group_id=group_id,
|
908
|
+
run_id=run_id,
|
737
909
|
)
|
738
910
|
|
739
911
|
def to_openai_dict_search_results(self, max_tool_id_length: int = TOOL_CALL_ID_MAX_LEN) -> dict:
|
@@ -746,8 +918,13 @@ class Message(BaseMessage):
|
|
746
918
|
max_tool_id_length: int = TOOL_CALL_ID_MAX_LEN,
|
747
919
|
put_inner_thoughts_in_kwargs: bool = False,
|
748
920
|
use_developer_message: bool = False,
|
921
|
+
# if true, then treat the content field as AssistantMessage
|
922
|
+
native_content: bool = False,
|
923
|
+
strip_request_heartbeat: bool = False,
|
749
924
|
) -> dict | None:
|
750
925
|
"""Go from Message class to ChatCompletion message object"""
|
926
|
+
assert not (native_content and put_inner_thoughts_in_kwargs), "native_content and put_inner_thoughts_in_kwargs cannot both be true"
|
927
|
+
|
751
928
|
if self.role == "approval" and self.tool_calls is None:
|
752
929
|
return None
|
753
930
|
|
@@ -763,8 +940,8 @@ class Message(BaseMessage):
|
|
763
940
|
# Otherwise, check if we have TextContent and multiple other parts
|
764
941
|
elif self.content and len(self.content) > 1:
|
765
942
|
text = [content for content in self.content if isinstance(content, TextContent)]
|
766
|
-
assert len(text) == 1, f"multiple text content parts found in a single message: {self.content}"
|
767
|
-
text_content =
|
943
|
+
# assert len(text) == 1, f"multiple text content parts found in a single message: {self.content}"
|
944
|
+
text_content = "\n\n".join([t.text for t in text])
|
768
945
|
parse_content_parts = True
|
769
946
|
else:
|
770
947
|
text_content = None
|
@@ -788,11 +965,28 @@ class Message(BaseMessage):
|
|
788
965
|
}
|
789
966
|
|
790
967
|
elif self.role == "assistant" or self.role == "approval":
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
795
|
-
|
968
|
+
try:
|
969
|
+
assert self.tool_calls is not None or text_content is not None, vars(self)
|
970
|
+
except AssertionError as e:
|
971
|
+
# relax check if this message only contains reasoning content
|
972
|
+
if self.content is not None and len(self.content) > 0 and isinstance(self.content[0], ReasoningContent):
|
973
|
+
return None
|
974
|
+
raise e
|
975
|
+
|
976
|
+
# if native content, then put it directly inside the content
|
977
|
+
if native_content:
|
978
|
+
openai_message = {
|
979
|
+
# TODO support listed content (if it's possible for role assistant?)
|
980
|
+
# "content": self.content,
|
981
|
+
"content": text_content, # here content is not reasoning, it's assistant message
|
982
|
+
"role": "assistant",
|
983
|
+
}
|
984
|
+
# otherwise, if inner_thoughts_in_kwargs, hold it for the tool calls
|
985
|
+
else:
|
986
|
+
openai_message = {
|
987
|
+
"content": None if (put_inner_thoughts_in_kwargs and self.tool_calls is not None) else text_content,
|
988
|
+
"role": "assistant",
|
989
|
+
}
|
796
990
|
|
797
991
|
if self.tool_calls is not None:
|
798
992
|
if put_inner_thoughts_in_kwargs:
|
@@ -807,6 +1001,11 @@ class Message(BaseMessage):
|
|
807
1001
|
]
|
808
1002
|
else:
|
809
1003
|
openai_message["tool_calls"] = [tool_call.model_dump() for tool_call in self.tool_calls]
|
1004
|
+
|
1005
|
+
if strip_request_heartbeat:
|
1006
|
+
for tool_call_dict in openai_message["tool_calls"]:
|
1007
|
+
tool_call_dict.pop(REQUEST_HEARTBEAT_PARAM, None)
|
1008
|
+
|
810
1009
|
if max_tool_id_length:
|
811
1010
|
for tool_call_dict in openai_message["tool_calls"]:
|
812
1011
|
tool_call_dict["id"] = tool_call_dict["id"][:max_tool_id_length]
|
@@ -847,6 +1046,7 @@ class Message(BaseMessage):
|
|
847
1046
|
put_inner_thoughts_in_kwargs: bool = False,
|
848
1047
|
use_developer_message: bool = False,
|
849
1048
|
) -> List[dict]:
|
1049
|
+
messages = Message.filter_messages_for_llm_api(messages)
|
850
1050
|
result = [
|
851
1051
|
m.to_openai_dict(
|
852
1052
|
max_tool_id_length=max_tool_id_length,
|
@@ -858,10 +1058,118 @@ class Message(BaseMessage):
|
|
858
1058
|
result = [m for m in result if m is not None]
|
859
1059
|
return result
|
860
1060
|
|
1061
|
+
def to_openai_responses_dicts(
|
1062
|
+
self,
|
1063
|
+
max_tool_id_length: int = TOOL_CALL_ID_MAX_LEN,
|
1064
|
+
) -> List[dict]:
|
1065
|
+
"""Go from Message class to ChatCompletion message object"""
|
1066
|
+
|
1067
|
+
if self.role == "approval" and self.tool_calls is None:
|
1068
|
+
return []
|
1069
|
+
|
1070
|
+
message_dicts = []
|
1071
|
+
|
1072
|
+
if self.role == "system":
|
1073
|
+
assert len(self.content) == 1 and isinstance(self.content[0], TextContent), vars(self)
|
1074
|
+
message_dicts.append(
|
1075
|
+
{
|
1076
|
+
"role": "developer",
|
1077
|
+
"content": self.content[0].text,
|
1078
|
+
}
|
1079
|
+
)
|
1080
|
+
|
1081
|
+
elif self.role == "user":
|
1082
|
+
# TODO do we need to do a swap to placeholder text here for images?
|
1083
|
+
assert all([isinstance(c, TextContent) or isinstance(c, ImageContent) for c in self.content]), vars(self)
|
1084
|
+
|
1085
|
+
user_dict = {
|
1086
|
+
"role": self.role.value if hasattr(self.role, "value") else self.role,
|
1087
|
+
# TODO support multi-modal
|
1088
|
+
"content": self.content[0].text,
|
1089
|
+
}
|
1090
|
+
|
1091
|
+
# Optional field, do not include if null or invalid
|
1092
|
+
if self.name is not None:
|
1093
|
+
if bool(re.match(r"^[^\s<|\\/>]+$", self.name)):
|
1094
|
+
user_dict["name"] = self.name
|
1095
|
+
else:
|
1096
|
+
warnings.warn(f"Using OpenAI with invalid 'name' field (name={self.name} role={self.role}).")
|
1097
|
+
|
1098
|
+
message_dicts.append(user_dict)
|
1099
|
+
|
1100
|
+
elif self.role == "assistant" or self.role == "approval":
|
1101
|
+
assert self.tool_calls is not None or (self.content is not None and len(self.content) > 0)
|
1102
|
+
|
1103
|
+
# A few things may be in here, firstly reasoning content, secondly assistant messages, thirdly tool calls
|
1104
|
+
# TODO check if OpenAI Responses is capable of R->A->T like Anthropic?
|
1105
|
+
|
1106
|
+
if self.content is not None:
|
1107
|
+
for content_part in self.content:
|
1108
|
+
if isinstance(content_part, SummarizedReasoningContent):
|
1109
|
+
message_dicts.append(
|
1110
|
+
{
|
1111
|
+
"type": "reasoning",
|
1112
|
+
"id": content_part.id,
|
1113
|
+
"summary": [{"type": "summary_text", "text": s.text} for s in content_part.summary],
|
1114
|
+
"encrypted_content": content_part.encrypted_content,
|
1115
|
+
}
|
1116
|
+
)
|
1117
|
+
elif isinstance(content_part, TextContent):
|
1118
|
+
message_dicts.append(
|
1119
|
+
{
|
1120
|
+
"role": "assistant",
|
1121
|
+
"content": content_part.text,
|
1122
|
+
}
|
1123
|
+
)
|
1124
|
+
# else skip
|
1125
|
+
|
1126
|
+
if self.tool_calls is not None:
|
1127
|
+
for tool_call in self.tool_calls:
|
1128
|
+
message_dicts.append(
|
1129
|
+
{
|
1130
|
+
"type": "function_call",
|
1131
|
+
"call_id": tool_call.id[:max_tool_id_length] if max_tool_id_length else tool_call.id,
|
1132
|
+
"name": tool_call.function.name,
|
1133
|
+
"arguments": tool_call.function.arguments,
|
1134
|
+
"status": "completed", # TODO check if needed?
|
1135
|
+
}
|
1136
|
+
)
|
1137
|
+
|
1138
|
+
elif self.role == "tool":
|
1139
|
+
assert self.tool_call_id is not None, vars(self)
|
1140
|
+
assert len(self.content) == 1 and isinstance(self.content[0], TextContent), vars(self)
|
1141
|
+
message_dicts.append(
|
1142
|
+
{
|
1143
|
+
"type": "function_call_output",
|
1144
|
+
"call_id": self.tool_call_id[:max_tool_id_length] if max_tool_id_length else self.tool_call_id,
|
1145
|
+
"output": self.content[0].text,
|
1146
|
+
}
|
1147
|
+
)
|
1148
|
+
|
1149
|
+
else:
|
1150
|
+
raise ValueError(self.role)
|
1151
|
+
|
1152
|
+
return message_dicts
|
1153
|
+
|
1154
|
+
@staticmethod
|
1155
|
+
def to_openai_responses_dicts_from_list(
|
1156
|
+
messages: List[Message],
|
1157
|
+
max_tool_id_length: int = TOOL_CALL_ID_MAX_LEN,
|
1158
|
+
) -> List[dict]:
|
1159
|
+
messages = Message.filter_messages_for_llm_api(messages)
|
1160
|
+
result = []
|
1161
|
+
for message in messages:
|
1162
|
+
result.extend(message.to_openai_responses_dicts(max_tool_id_length=max_tool_id_length))
|
1163
|
+
return result
|
1164
|
+
|
861
1165
|
def to_anthropic_dict(
|
862
1166
|
self,
|
1167
|
+
current_model: str,
|
863
1168
|
inner_thoughts_xml_tag="thinking",
|
864
1169
|
put_inner_thoughts_in_kwargs: bool = False,
|
1170
|
+
# if true, then treat the content field as AssistantMessage
|
1171
|
+
native_content: bool = False,
|
1172
|
+
strip_request_heartbeat: bool = False,
|
865
1173
|
) -> dict | None:
|
866
1174
|
"""
|
867
1175
|
Convert to an Anthropic message dictionary
|
@@ -869,6 +1177,8 @@ class Message(BaseMessage):
|
|
869
1177
|
Args:
|
870
1178
|
inner_thoughts_xml_tag (str): The XML tag to wrap around inner thoughts
|
871
1179
|
"""
|
1180
|
+
assert not (native_content and put_inner_thoughts_in_kwargs), "native_content and put_inner_thoughts_in_kwargs cannot both be true"
|
1181
|
+
|
872
1182
|
if self.role == "approval" and self.tool_calls is None:
|
873
1183
|
return None
|
874
1184
|
|
@@ -929,43 +1239,80 @@ class Message(BaseMessage):
|
|
929
1239
|
}
|
930
1240
|
|
931
1241
|
elif self.role == "assistant" or self.role == "approval":
|
932
|
-
assert self.tool_calls is not None or text_content is not None
|
1242
|
+
# assert self.tool_calls is not None or text_content is not None, vars(self)
|
1243
|
+
assert self.tool_calls is not None or len(self.content) > 0
|
933
1244
|
anthropic_message = {
|
934
1245
|
"role": "assistant",
|
935
1246
|
}
|
936
1247
|
content = []
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
945
|
-
|
946
|
-
|
947
|
-
|
948
|
-
|
949
|
-
|
950
|
-
|
951
|
-
|
952
|
-
|
953
|
-
|
954
|
-
|
955
|
-
|
956
|
-
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
|
961
|
-
|
962
|
-
|
963
|
-
|
964
|
-
|
965
|
-
|
966
|
-
|
967
|
-
|
968
|
-
|
1248
|
+
if native_content:
|
1249
|
+
# No special handling for TextContent
|
1250
|
+
if self.content is not None:
|
1251
|
+
for content_part in self.content:
|
1252
|
+
# TextContent, ImageContent, ToolCallContent, ToolReturnContent, ReasoningContent, RedactedReasoningContent, OmittedReasoningContent
|
1253
|
+
if isinstance(content_part, ReasoningContent):
|
1254
|
+
if current_model == self.model:
|
1255
|
+
content.append(
|
1256
|
+
{
|
1257
|
+
"type": "thinking",
|
1258
|
+
"thinking": content_part.reasoning,
|
1259
|
+
"signature": content_part.signature,
|
1260
|
+
}
|
1261
|
+
)
|
1262
|
+
elif isinstance(content_part, RedactedReasoningContent):
|
1263
|
+
if current_model == self.model:
|
1264
|
+
content.append(
|
1265
|
+
{
|
1266
|
+
"type": "redacted_thinking",
|
1267
|
+
"data": content_part.data,
|
1268
|
+
}
|
1269
|
+
)
|
1270
|
+
elif isinstance(content_part, TextContent):
|
1271
|
+
content.append(
|
1272
|
+
{
|
1273
|
+
"type": "text",
|
1274
|
+
"text": content_part.text,
|
1275
|
+
}
|
1276
|
+
)
|
1277
|
+
else:
|
1278
|
+
# Skip unsupported types eg OmmitedReasoningContent
|
1279
|
+
pass
|
1280
|
+
|
1281
|
+
else:
|
1282
|
+
# COT / reasoning / thinking
|
1283
|
+
if self.content is not None and len(self.content) >= 1:
|
1284
|
+
for content_part in self.content:
|
1285
|
+
if isinstance(content_part, ReasoningContent):
|
1286
|
+
if current_model == self.model:
|
1287
|
+
content.append(
|
1288
|
+
{
|
1289
|
+
"type": "thinking",
|
1290
|
+
"thinking": content_part.reasoning,
|
1291
|
+
"signature": content_part.signature,
|
1292
|
+
}
|
1293
|
+
)
|
1294
|
+
if isinstance(content_part, RedactedReasoningContent):
|
1295
|
+
if current_model == self.model:
|
1296
|
+
content.append(
|
1297
|
+
{
|
1298
|
+
"type": "redacted_thinking",
|
1299
|
+
"data": content_part.data,
|
1300
|
+
}
|
1301
|
+
)
|
1302
|
+
if isinstance(content_part, TextContent):
|
1303
|
+
content.append(
|
1304
|
+
{
|
1305
|
+
"type": "text",
|
1306
|
+
"text": content_part.text,
|
1307
|
+
}
|
1308
|
+
)
|
1309
|
+
elif text_content is not None:
|
1310
|
+
content.append(
|
1311
|
+
{
|
1312
|
+
"type": "text",
|
1313
|
+
"text": add_xml_tag(string=text_content, xml_tag=inner_thoughts_xml_tag),
|
1314
|
+
}
|
1315
|
+
)
|
969
1316
|
# Tool calling
|
970
1317
|
if self.tool_calls is not None:
|
971
1318
|
for tool_call in self.tool_calls:
|
@@ -978,6 +1325,9 @@ class Message(BaseMessage):
|
|
978
1325
|
else:
|
979
1326
|
tool_call_input = parse_json(tool_call.function.arguments)
|
980
1327
|
|
1328
|
+
if strip_request_heartbeat:
|
1329
|
+
tool_call_input.pop(REQUEST_HEARTBEAT_PARAM, None)
|
1330
|
+
|
981
1331
|
content.append(
|
982
1332
|
{
|
983
1333
|
"type": "tool_use",
|
@@ -987,8 +1337,6 @@ class Message(BaseMessage):
|
|
987
1337
|
}
|
988
1338
|
)
|
989
1339
|
|
990
|
-
# If the only content was text, unpack it back into a singleton
|
991
|
-
# TODO support multi-modal
|
992
1340
|
anthropic_message["content"] = content
|
993
1341
|
|
994
1342
|
elif self.role == "tool":
|
@@ -1014,23 +1362,40 @@ class Message(BaseMessage):
|
|
1014
1362
|
@staticmethod
|
1015
1363
|
def to_anthropic_dicts_from_list(
|
1016
1364
|
messages: List[Message],
|
1365
|
+
current_model: str,
|
1017
1366
|
inner_thoughts_xml_tag: str = "thinking",
|
1018
1367
|
put_inner_thoughts_in_kwargs: bool = False,
|
1368
|
+
# if true, then treat the content field as AssistantMessage
|
1369
|
+
native_content: bool = False,
|
1370
|
+
strip_request_heartbeat: bool = False,
|
1019
1371
|
) -> List[dict]:
|
1372
|
+
messages = Message.filter_messages_for_llm_api(messages)
|
1020
1373
|
result = [
|
1021
1374
|
m.to_anthropic_dict(
|
1375
|
+
current_model=current_model,
|
1022
1376
|
inner_thoughts_xml_tag=inner_thoughts_xml_tag,
|
1023
1377
|
put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
|
1378
|
+
native_content=native_content,
|
1379
|
+
strip_request_heartbeat=strip_request_heartbeat,
|
1024
1380
|
)
|
1025
1381
|
for m in messages
|
1026
1382
|
]
|
1027
1383
|
result = [m for m in result if m is not None]
|
1028
1384
|
return result
|
1029
1385
|
|
1030
|
-
def to_google_dict(
|
1386
|
+
def to_google_dict(
|
1387
|
+
self,
|
1388
|
+
current_model: str,
|
1389
|
+
put_inner_thoughts_in_kwargs: bool = True,
|
1390
|
+
# if true, then treat the content field as AssistantMessage
|
1391
|
+
native_content: bool = False,
|
1392
|
+
strip_request_heartbeat: bool = False,
|
1393
|
+
) -> dict | None:
|
1031
1394
|
"""
|
1032
1395
|
Go from Message class to Google AI REST message object
|
1033
1396
|
"""
|
1397
|
+
assert not (native_content and put_inner_thoughts_in_kwargs), "native_content and put_inner_thoughts_in_kwargs cannot both be true"
|
1398
|
+
|
1034
1399
|
if self.role == "approval" and self.tool_calls is None:
|
1035
1400
|
return None
|
1036
1401
|
|
@@ -1080,7 +1445,7 @@ class Message(BaseMessage):
|
|
1080
1445
|
}
|
1081
1446
|
|
1082
1447
|
elif self.role == "assistant" or self.role == "approval":
|
1083
|
-
assert self.tool_calls is not None or text_content is not None
|
1448
|
+
assert self.tool_calls is not None or text_content is not None or len(self.content) > 1
|
1084
1449
|
google_ai_message = {
|
1085
1450
|
"role": "model", # NOTE: different
|
1086
1451
|
}
|
@@ -1088,7 +1453,12 @@ class Message(BaseMessage):
|
|
1088
1453
|
# NOTE: Google AI API doesn't allow non-null content + function call
|
1089
1454
|
# To get around this, just two a two part message, inner thoughts first then
|
1090
1455
|
parts = []
|
1091
|
-
|
1456
|
+
|
1457
|
+
if native_content and text_content is not None:
|
1458
|
+
# TODO support multi-part assistant content
|
1459
|
+
parts.append({"text": text_content})
|
1460
|
+
|
1461
|
+
elif not put_inner_thoughts_in_kwargs and text_content is not None:
|
1092
1462
|
# NOTE: ideally we do multi-part for CoT / inner thoughts + function call, but Google AI API doesn't allow it
|
1093
1463
|
raise NotImplementedError
|
1094
1464
|
parts.append({"text": text_content})
|
@@ -1110,6 +1480,9 @@ class Message(BaseMessage):
|
|
1110
1480
|
assert len(self.tool_calls) == 1
|
1111
1481
|
function_args[INNER_THOUGHTS_KWARG_VERTEX] = text_content
|
1112
1482
|
|
1483
|
+
if strip_request_heartbeat:
|
1484
|
+
function_args.pop(REQUEST_HEARTBEAT_PARAM, None)
|
1485
|
+
|
1113
1486
|
parts.append(
|
1114
1487
|
{
|
1115
1488
|
"functionCall": {
|
@@ -1119,8 +1492,37 @@ class Message(BaseMessage):
|
|
1119
1492
|
}
|
1120
1493
|
)
|
1121
1494
|
else:
|
1122
|
-
|
1123
|
-
|
1495
|
+
if not native_content:
|
1496
|
+
assert text_content is not None
|
1497
|
+
parts.append({"text": text_content})
|
1498
|
+
|
1499
|
+
if self.content and len(self.content) > 1:
|
1500
|
+
native_google_content_parts = []
|
1501
|
+
for content in self.content:
|
1502
|
+
if isinstance(content, TextContent):
|
1503
|
+
native_part = {"text": content.text}
|
1504
|
+
if content.signature and current_model == self.model:
|
1505
|
+
native_part["thought_signature"] = content.signature
|
1506
|
+
native_google_content_parts.append(native_part)
|
1507
|
+
elif isinstance(content, ReasoningContent):
|
1508
|
+
if current_model == self.model:
|
1509
|
+
native_google_content_parts.append({"text": content.reasoning, "thought": True})
|
1510
|
+
elif isinstance(content, ToolCallContent):
|
1511
|
+
native_part = {
|
1512
|
+
"function_call": {
|
1513
|
+
"name": content.name,
|
1514
|
+
"args": content.input,
|
1515
|
+
},
|
1516
|
+
}
|
1517
|
+
if content.signature and current_model == self.model:
|
1518
|
+
native_part["thought_signature"] = content.signature
|
1519
|
+
native_google_content_parts.append(native_part)
|
1520
|
+
else:
|
1521
|
+
# silently drop other content types
|
1522
|
+
pass
|
1523
|
+
if native_google_content_parts:
|
1524
|
+
parts = native_google_content_parts
|
1525
|
+
|
1124
1526
|
google_ai_message["parts"] = parts
|
1125
1527
|
|
1126
1528
|
elif self.role == "tool":
|
@@ -1170,17 +1572,61 @@ class Message(BaseMessage):
|
|
1170
1572
|
@staticmethod
|
1171
1573
|
def to_google_dicts_from_list(
|
1172
1574
|
messages: List[Message],
|
1575
|
+
current_model: str,
|
1173
1576
|
put_inner_thoughts_in_kwargs: bool = True,
|
1577
|
+
native_content: bool = False,
|
1174
1578
|
):
|
1579
|
+
messages = Message.filter_messages_for_llm_api(messages)
|
1175
1580
|
result = [
|
1176
1581
|
m.to_google_dict(
|
1582
|
+
current_model=current_model,
|
1177
1583
|
put_inner_thoughts_in_kwargs=put_inner_thoughts_in_kwargs,
|
1584
|
+
native_content=native_content,
|
1178
1585
|
)
|
1179
1586
|
for m in messages
|
1180
1587
|
]
|
1181
1588
|
result = [m for m in result if m is not None]
|
1182
1589
|
return result
|
1183
1590
|
|
1591
|
+
def is_approval_request(self) -> bool:
|
1592
|
+
return self.role == "approval" and self.tool_calls is not None and len(self.tool_calls) > 0
|
1593
|
+
|
1594
|
+
def is_approval_response(self) -> bool:
|
1595
|
+
return self.role == "approval" and self.tool_calls is None and self.approve is not None
|
1596
|
+
|
1597
|
+
def is_summarization_message(self) -> bool:
|
1598
|
+
return (
|
1599
|
+
self.role == "user"
|
1600
|
+
and self.content is not None
|
1601
|
+
and len(self.content) == 1
|
1602
|
+
and isinstance(self.content[0], TextContent)
|
1603
|
+
and "system_alert" in self.content[0].text
|
1604
|
+
)
|
1605
|
+
|
1606
|
+
@staticmethod
|
1607
|
+
def filter_messages_for_llm_api(
|
1608
|
+
messages: List[Message],
|
1609
|
+
) -> List[Message]:
|
1610
|
+
messages = [m for m in messages if m is not None]
|
1611
|
+
if len(messages) == 0:
|
1612
|
+
return []
|
1613
|
+
# Add special handling for legacy bug where summarization triggers in the middle of hitl
|
1614
|
+
messages_to_filter = []
|
1615
|
+
for i in range(len(messages) - 1):
|
1616
|
+
first_message_is_approval = messages[i].is_approval_request()
|
1617
|
+
second_message_is_summary = messages[i + 1].is_summarization_message()
|
1618
|
+
third_message_is_optional_approval = i + 2 >= len(messages) or messages[i + 2].is_approval_response()
|
1619
|
+
if first_message_is_approval and second_message_is_summary and third_message_is_optional_approval:
|
1620
|
+
messages_to_filter.append(messages[i])
|
1621
|
+
for idx in reversed(messages_to_filter): # reverse to avoid index shift
|
1622
|
+
messages.remove(idx)
|
1623
|
+
|
1624
|
+
# Filter last message if it is a lone approval request without a response - this only occurs for token counting
|
1625
|
+
if messages[-1].role == "approval" and messages[-1].tool_calls is not None and len(messages[-1].tool_calls) > 0:
|
1626
|
+
messages.remove(messages[-1])
|
1627
|
+
|
1628
|
+
return messages
|
1629
|
+
|
1184
1630
|
@staticmethod
|
1185
1631
|
def generate_otid_from_id(message_id: str, index: int) -> str:
|
1186
1632
|
"""
|
@@ -1200,10 +1646,11 @@ class Message(BaseMessage):
|
|
1200
1646
|
|
1201
1647
|
|
1202
1648
|
class ToolReturn(BaseModel):
|
1649
|
+
tool_call_id: Optional[Any] = Field(None, description="The ID for the tool call")
|
1203
1650
|
status: Literal["success", "error"] = Field(..., description="The status of the tool call")
|
1204
1651
|
stdout: Optional[List[str]] = Field(default=None, description="Captured stdout (e.g. prints, logs) from the tool invocation")
|
1205
1652
|
stderr: Optional[List[str]] = Field(default=None, description="Captured stderr from the tool invocation")
|
1206
|
-
|
1653
|
+
func_response: Optional[str] = Field(None, description="The function response string")
|
1207
1654
|
|
1208
1655
|
|
1209
1656
|
class MessageSearchRequest(BaseModel):
|