Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Containerfile.c10s
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ RUN dnf -y install --allowerasing \
${EXTRA_PACKAGES} \
&& dnf clean all

COPY beeai-reasoning.patch /tmp
COPY openinference-reasoning.patch /tmp

RUN pip3 install --no-cache-dir \
"litellm!=1.82.7,!=1.82.8" \
beeai-framework[vertexai,mcp,duckduckgo]==0.1.80 \
Expand All @@ -66,7 +69,10 @@ RUN pip3 install --no-cache-dir \
specfile \
pytest \
pytest-asyncio \
GitPython>=3.1.0
GitPython>=3.1.0 \
&& cd /usr/local/lib/python3.12/site-packages \
&& patch -p2 -i /tmp/beeai-reasoning.patch \
&& patch -p5 -i /tmp/openinference-reasoning.patch

# Verify no malicious litellm_init.pth was introduced by compromised litellm packages (e.g. 1.82.7, 1.82.8)
RUN MALICIOUS=$(find /usr /opt -name "litellm_init.pth" 2>/dev/null); \
Expand Down
8 changes: 7 additions & 1 deletion Containerfile.c9s
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ RUN dnf -y install --allowerasing \
${EXTRA_PACKAGES} \
&& dnf clean all

COPY beeai-reasoning.patch /tmp
COPY openinference-reasoning.patch /tmp

# Create Python 3.11 virtual environment and install Python packages
RUN python3.11 -m venv --system-site-packages /opt/beeai-venv \
&& /opt/beeai-venv/bin/pip install --upgrade pip \
Expand All @@ -67,7 +70,10 @@ RUN python3.11 -m venv --system-site-packages /opt/beeai-venv \
redis \
specfile \
koji \
GitPython>=3.1.0
GitPython>=3.1.0 \
&& cd /opt/beeai-venv/lib/python3.11/site-packages \
&& patch -p2 -i /tmp/beeai-reasoning.patch \
&& patch -p5 -i /tmp/openinference-reasoning.patch

# Verify no malicious litellm_init.pth was introduced by compromised litellm packages (e.g. 1.82.7, 1.82.8)
RUN MALICIOUS=$(find /usr /opt -name "litellm_init.pth" 2>/dev/null); \
Expand Down
201 changes: 201 additions & 0 deletions beeai-reasoning.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
diff --git a/python/beeai_framework/adapters/litellm/chat.py b/python/beeai_framework/adapters/litellm/chat.py
index b5c5a9b4..9ad9f544 100644
--- a/python/beeai_framework/adapters/litellm/chat.py
+++ b/python/beeai_framework/adapters/litellm/chat.py
@@ -37,7 +37,10 @@ from beeai_framework.backend.chat import (
)
from beeai_framework.backend.errors import ChatModelError
from beeai_framework.backend.message import (
+ AnyMessage,
AssistantMessage,
+ AssistantMessageContent,
+ MessageReasoningContent,
MessageTextContent,
MessageToolCallContent,
ToolMessage,
@@ -195,6 +198,7 @@ class LiteLLMChatModel(ChatModel, ABC):
"role": "assistant",
"content": msg_text_content or None,
"tool_calls": msg_tool_calls or None,
+ "thinking_blocks": message.meta.get("thinking_blocks"),
}
if self.model_supports_tool_calling
else {
@@ -307,27 +311,41 @@ class LiteLLMChatModel(ChatModel, ABC):
total_cost_usd=prompt_tokens_cost_usd + completion_tokens_cost_usd,
)

- return ChatModelOutput(
- output=(
- [
- AssistantMessage(
- [
- MessageToolCallContent(
- id=call.id or "",
- tool_name=call.function.name or "",
- args=call.function.arguments,
- )
- for call in update.tool_calls
- ],
- id=chunk.id,
+ reasoning_content = getattr(update, "reasoning_content", None) if update else None
+ # Anthropic requires `thinking_blocks` (with cryptographic signatures) to be sent back
+ # in conversation history; without them LiteLLM silently disables thinking on follow-up turns
+ meta = None
+ if (thinking_blocks := (getattr(update, "thinking_blocks", None) if update else None)) and (
+ # Streaming deltas carry partial blocks without signatures - filter those out
+ signed_thinking_blocks := [
+ b
+ for b in thinking_blocks
+ if (b.get("signature") if isinstance(b, dict) else getattr(b, "signature", None))
+ ]
+ ):
+ meta = {"thinking_blocks": signed_thinking_blocks}
+
+ if update:
+ parts: list[AssistantMessageContent] = []
+ if reasoning_content:
+ parts.append(MessageReasoningContent(text=reasoning_content))
+ if update.tool_calls:
+ parts.extend(
+ MessageToolCallContent(
+ id=call.id or "",
+ tool_name=call.function.name or "",
+ args=call.function.arguments,
)
- if update.tool_calls
- # pyrefly: ignore [bad-argument-type]
- else AssistantMessage(update.content or update.reasoning_content or "", id=chunk.id)
- ]
- if (update and update.model_dump(exclude_none=True))
- else []
- ),
+ for call in update.tool_calls
+ )
+ if update.content:
+ parts.append(MessageTextContent(text=update.content))
+ output: list[AnyMessage] = [AssistantMessage(parts, id=chunk.id, meta=meta)] if parts or meta else []
+ else:
+ output: list[AnyMessage] = []
+
+ return ChatModelOutput(
+ output=output,
# Will be set later
output_structured=None,
finish_reason=finish_reason,
diff --git a/python/beeai_framework/backend/__init__.py b/python/beeai_framework/backend/__init__.py
index fe8e5002..f3d7fe86 100644
--- a/python/beeai_framework/backend/__init__.py
+++ b/python/beeai_framework/backend/__init__.py
@@ -23,6 +23,7 @@ from beeai_framework.backend.message import (
Message,
MessageFileContent,
MessageImageContent,
+ MessageReasoningContent,
MessageTextContent,
MessageToolCallContent,
MessageToolResultContent,
@@ -65,6 +66,7 @@ __all__ = [
"MessageError",
"MessageFileContent",
"MessageImageContent",
+ "MessageReasoningContent",
"MessageTextContent",
"MessageToolCallContent",
"MessageToolResultContent",
diff --git a/python/beeai_framework/backend/chat.py b/python/beeai_framework/backend/chat.py
index 972b5246..a1ed0ca3 100644
--- a/python/beeai_framework/backend/chat.py
+++ b/python/beeai_framework/backend/chat.py
@@ -229,6 +229,11 @@ class ChatModelOptions(RunnableOptions, total=False):
Generated chunks will be streamed without validation of the produced tool calls.
"""

+ reasoning_effort: str | None
+ """
+ Controls the amount of reasoning effort for models that support it (e.g., "low", "medium", "high").
+ """
+
fallback_tool: AnyTool | None
"""
Tool to invoke when the model makes a malformed tool call (for example, when it forgets the name of a tool).
diff --git a/python/beeai_framework/backend/message.py b/python/beeai_framework/backend/message.py
index 3877d35b..befd7e9e 100644
--- a/python/beeai_framework/backend/message.py
+++ b/python/beeai_framework/backend/message.py
@@ -86,6 +86,11 @@ class MessageToolResultContent(BaseModel):
tool_call_id: str


+class MessageReasoningContent(BaseModel):
+ type: Literal["reasoning"] = "reasoning"
+ text: str
+
+
class MessageToolCallContent(BaseModel):
type: Literal["tool-call"] = "tool-call"
id: str
@@ -157,7 +162,7 @@ class Message(ABC, Generic[T]):
return type(self)([c.model_copy() for c in self.content], self.meta.copy())


-AssistantMessageContent = MessageTextContent | MessageToolCallContent
+AssistantMessageContent = MessageTextContent | MessageToolCallContent | MessageReasoningContent


class AssistantMessage(Message[AssistantMessageContent]):
@@ -175,8 +180,10 @@ class AssistantMessage(Message[AssistantMessageContent]):
(
MessageTextContent(text=c)
if isinstance(c, str)
- # pyrefly: ignore [bad-argument-type]
- else to_any_model([MessageToolCallContent, MessageTextContent], cast(AssistantMessageContent, c))
+ else to_any_model(
+ [MessageToolCallContent, MessageReasoningContent, MessageTextContent],
+ cast(AssistantMessageContent, c), # pyrefly: ignore [bad-argument-type]
+ )
)
for c in cast_list(content)
]
@@ -189,12 +196,19 @@ class AssistantMessage(Message[AssistantMessageContent]):
id=id,
)

+ @property
+ def reasoning(self) -> str:
+ return "".join([x.text for x in self.get_reasoning_messages()])
+
def get_tool_calls(self) -> list[MessageToolCallContent]:
return [cont for cont in self.content if isinstance(cont, MessageToolCallContent)]

def get_text_messages(self) -> list[MessageTextContent]:
return [cont for cont in self.content if isinstance(cont, MessageTextContent)]

+ def get_reasoning_messages(self) -> list[MessageReasoningContent]:
+ return [cont for cont in self.content if isinstance(cont, MessageReasoningContent)]
+

class ToolMessage(Message[MessageToolResultContent]):
role = Role.TOOL
diff --git a/python/beeai_framework/backend/types.py b/python/beeai_framework/backend/types.py
index b44a0a19..222d60a7 100644
--- a/python/beeai_framework/backend/types.py
+++ b/python/beeai_framework/backend/types.py
@@ -33,6 +33,7 @@ class ChatModelParameters(BaseModel):
seed: int | None = None
stop_sequences: list[str] | None = None
stream: bool | None = None
+ reasoning_effort: str | None = None


class ChatModelStructureInput(ChatModelParameters, Generic[T]):
@@ -218,6 +219,9 @@ class ChatModelOutput(RunnableOutput):
def get_text_content(self) -> str:
return "".join([x.text for x in list(filter(lambda x: isinstance(x, AssistantMessage), self.output))])

+ def get_reasoning_content(self) -> str:
+ return "".join([x.reasoning for x in self.output if isinstance(x, AssistantMessage)])
+

ChatModelCache = BaseCache[list[ChatModelOutput]]

44 changes: 44 additions & 0 deletions openinference-reasoning.patch
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
diff --git a/python/instrumentation/openinference-instrumentation-beeai/src/openinference/instrumentation/beeai/processors/chat.py b/python/instrumentation/openinference-instrumentation-beeai/src/openinference/instrumentation/beeai/processors/chat.py
index a135b4b7..a8a7a0f4 100644
--- a/python/instrumentation/openinference-instrumentation-beeai/src/openinference/instrumentation/beeai/processors/chat.py
+++ b/python/instrumentation/openinference-instrumentation-beeai/src/openinference/instrumentation/beeai/processors/chat.py
@@ -5,6 +5,7 @@ from beeai_framework.backend import (
AnyMessage,
ChatModel,
MessageImageContent,
+ MessageReasoningContent,
MessageTextContent,
MessageToolCallContent,
MessageToolResultContent,
@@ -218,6 +219,11 @@ def _process_messages(
),
}
if isinstance(content, MessageToolResultContent)
+ else {
+ MessageContentAttributes.MESSAGE_CONTENT_TYPE: "reasoning",
+ MessageContentAttributes.MESSAGE_CONTENT_TEXT: content.text,
+ }
+ if isinstance(content, MessageReasoningContent)
else None
)
for content in msg.content
@@ -244,7 +250,7 @@ def _process_messages(


def _aggregate_msg_content(message: "AnyMessage") -> None:
- from beeai_framework.backend import MessageTextContent, MessageToolCallContent
+ from beeai_framework.backend import MessageReasoningContent, MessageTextContent, MessageToolCallContent

contents = message.content.copy()
aggregated_content: list[Any] = []
@@ -257,6 +263,10 @@ def _aggregate_msg_content(message: "AnyMessage") -> None:
content, MessageToolCallContent
):
last_content.args += content.args
+ elif isinstance(last_content, MessageReasoningContent) and isinstance(
+ content, MessageReasoningContent
+ ):
+ last_content.text += content.text
else:
aggregated_content.append(content)

3 changes: 3 additions & 0 deletions templates/beeai-agent.env
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ CHAT_MODEL=
# CHAT_MODEL_REBASE=
# CHAT_MODEL_REBUILD=

# One of: none, minimal, low, medium, high; defaults to none
#REASONING_EFFORT=high

# =============================================================================
# CREDENTIALS - Use based on model prefix above
# =============================================================================
Expand Down
8 changes: 5 additions & 3 deletions ymir/agents/backport_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
from pathlib import Path
from typing import Any

from beeai_framework.agents.requirement import RequirementAgent
from beeai_framework.agents.requirement.requirements.conditional import (
ConditionalRequirement,
)
Expand All @@ -29,12 +28,14 @@
from ymir.agents.log_agent import get_prompt as get_log_prompt
from ymir.agents.observability import setup_observability
from ymir.agents.package_update_steps import PackageUpdateState, PackageUpdateStep
from ymir.agents.reasoning_agent import ReasoningAgent
from ymir.agents.utils import (
check_subprocess,
format_mr_justification,
get_agent_execution_config,
get_chat_model,
get_tool_call_checker_config,
is_reasoning_enabled,
mcp_tools,
render_prompt,
resolve_chat_model_override,
Expand Down Expand Up @@ -885,7 +886,7 @@ async def create_backport_agent(
local_tool_options: dict[str, Any],
include_build_tools: bool = False,
fix_version: str | None = None,
) -> RequirementAgent:
) -> ReasoningAgent:
"""
Create a backport agent.

Expand Down Expand Up @@ -934,9 +935,10 @@ async def create_backport_agent(
if include_build_tools:
base_tools.extend([t for t in mcp_tools if t.name in ["build_package", "download_artifacts"]])

return RequirementAgent(
return ReasoningAgent(
name="BackportAgent",
llm=get_chat_model(),
unconstrained=is_reasoning_enabled(),
tool_call_checker=get_tool_call_checker_config(),
tools=base_tools,
memory=UnconstrainedMemory(),
Expand Down
9 changes: 5 additions & 4 deletions ymir/agents/build_agent.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
from typing import Any

from beeai_framework.agents.requirement import RequirementAgent
from beeai_framework.agents.requirement.requirements.conditional import (
ConditionalRequirement,
)
Expand All @@ -10,7 +9,8 @@
from beeai_framework.tools.search.duckduckgo import DuckDuckGoSearchTool
from beeai_framework.tools.think import ThinkTool

from ymir.agents.utils import get_chat_model, get_tool_call_checker_config
from ymir.agents.reasoning_agent import ReasoningAgent
from ymir.agents.utils import get_chat_model, get_tool_call_checker_config, is_reasoning_enabled
from ymir.tools.unprivileged.commands import RunShellCommandTool
from ymir.tools.unprivileged.filesystem import GetCWDTool
from ymir.tools.unprivileged.text import (
Expand Down Expand Up @@ -49,10 +49,11 @@ def get_prompt() -> str:
"""


def create_build_agent(mcp_tools: list[Tool], local_tool_options: dict[str, Any]) -> RequirementAgent:
return RequirementAgent(
def create_build_agent(mcp_tools: list[Tool], local_tool_options: dict[str, Any]) -> ReasoningAgent:
return ReasoningAgent(
name="BuildAgent",
llm=get_chat_model(),
unconstrained=is_reasoning_enabled(),
tool_call_checker=get_tool_call_checker_config(),
tools=[
ThinkTool(),
Expand Down
Loading
Loading