mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-11-12 22:20:33 +00:00
* refactor(agent): ground runtime decisions in structured evidence Use producer status and structured errors for tool recovery, preserve memory context without lexical confirmation, and remove predetermined receipt and skill conclusions. Require explicit MCP discovery and authenticated schedule trial actions. Preserve authorization, ownership, isolation, and bounded execution; update documentation and regression coverage. * test(sandbox): expect read_file initialization error callback * fix(agent): address evidence review follow-ups * fix(agent): preserve retry budgets for OpenAI transport errors * fix(agent): expose tool execution status in model requests Project validated execution metadata and exact tool text into request-only JSON so status survives provider serialization. Preserve stored results and receipt hashes, cover SDK and graph boundaries, and migrate replay input hashes without changing recorded responses. --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
162 lines
6.1 KiB
Python
162 lines
6.1 KiB
Python
"""Patched ChatOpenAI adapter for StepFun reasoning models.
|
|
|
|
StepFun returns ``reasoning`` (or ``reasoning_content`` with deepseek-style) in
|
|
both streaming deltas and non-streaming responses. Standard ``ChatOpenAI``
|
|
ignores these non-standard fields, so reasoning content is silently dropped.
|
|
This adapter captures reasoning from all response paths and replays it on
|
|
historical assistant messages for multi-turn tool-call conversations.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Mapping
|
|
from typing import Any
|
|
|
|
from langchain_core.language_models import LanguageModelInput
|
|
from langchain_core.messages import AIMessage, AIMessageChunk
|
|
from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult
|
|
|
|
from deerflow.models.assistant_payload_replay import (
|
|
restore_assistant_payloads,
|
|
restore_reasoning_content,
|
|
)
|
|
from deerflow.models.openai_response import GuardedChatOpenAI
|
|
from deerflow.models.reasoning_capture import typed_choice_message, with_reasoning_content
|
|
|
|
_MISSING = object()
|
|
|
|
|
|
def _extract_reasoning(value: Any) -> str | object:
|
|
"""Return reasoning content from a dict/Pydantic object.
|
|
|
|
StepFun may return reasoning via ``reasoning`` (default) or
|
|
``reasoning_content`` (deepseek-style). Check both fields.
|
|
"""
|
|
if isinstance(value, Mapping):
|
|
# Check reasoning_content first (deepseek-style), then reasoning (default)
|
|
for field in ("reasoning_content", "reasoning"):
|
|
if field in value and value[field] is not None:
|
|
return value[field]
|
|
return _MISSING
|
|
|
|
# Pydantic / SDK object attributes
|
|
for field in ("reasoning_content", "reasoning"):
|
|
attr = getattr(value, field, _MISSING)
|
|
if attr is not _MISSING and attr is not None:
|
|
return attr
|
|
|
|
# Some SDK versions store extra fields in model_extra
|
|
model_extra = getattr(value, "model_extra", None)
|
|
if isinstance(model_extra, Mapping):
|
|
for field in ("reasoning_content", "reasoning"):
|
|
if field in model_extra and model_extra[field] is not None:
|
|
return model_extra[field]
|
|
|
|
return _MISSING
|
|
|
|
|
|
class PatchedChatStepFun(GuardedChatOpenAI):
|
|
"""ChatOpenAI with full reasoning support for StepFun models.
|
|
|
|
Captures ``reasoning`` / ``reasoning_content`` from both streaming and
|
|
non-streaming responses and replays it on historical assistant messages in
|
|
multi-turn tool-call conversations.
|
|
"""
|
|
|
|
@classmethod
|
|
def is_lc_serializable(cls) -> bool:
|
|
return True
|
|
|
|
@property
|
|
def lc_secrets(self) -> dict[str, str]:
|
|
return {"api_key": "STEPFUN_API_KEY", "openai_api_key": "STEPFUN_API_KEY"}
|
|
|
|
# --- Request payload replay ---
|
|
|
|
def _get_request_payload(
|
|
self,
|
|
input_: LanguageModelInput,
|
|
*,
|
|
stop: list[str] | None = None,
|
|
**kwargs: Any,
|
|
) -> dict:
|
|
"""Restore ``reasoning_content`` on historical assistant messages."""
|
|
original_messages = self._convert_input(input_).to_messages()
|
|
payload = super()._get_request_payload(input_, stop=stop, **kwargs)
|
|
# StepFun documents max_tokens; ChatOpenAI emits the newer OpenAI name.
|
|
# Preserve the configured output bound on probes and ordinary chat calls.
|
|
if "max_completion_tokens" in payload:
|
|
payload["max_tokens"] = payload.pop("max_completion_tokens")
|
|
|
|
restore_assistant_payloads(
|
|
payload.get("messages", []),
|
|
original_messages,
|
|
restore_reasoning_content,
|
|
)
|
|
|
|
return payload
|
|
|
|
# --- Streaming reasoning capture ---
|
|
|
|
def _convert_chunk_to_generation_chunk(
|
|
self,
|
|
chunk: dict,
|
|
default_chunk_class: type,
|
|
base_generation_info: dict | None,
|
|
) -> ChatGenerationChunk | None:
|
|
"""Capture ``reasoning`` / ``reasoning_content`` from streaming deltas."""
|
|
generation_chunk = super()._convert_chunk_to_generation_chunk(
|
|
chunk,
|
|
default_chunk_class,
|
|
base_generation_info,
|
|
)
|
|
if generation_chunk is None:
|
|
return None
|
|
|
|
choices = chunk.get("choices", [])
|
|
if choices:
|
|
delta = choices[0].get("delta") or {}
|
|
reasoning = _extract_reasoning(delta)
|
|
if reasoning is not _MISSING and isinstance(generation_chunk.message, AIMessageChunk):
|
|
generation_chunk = ChatGenerationChunk(
|
|
message=with_reasoning_content(generation_chunk.message, reasoning),
|
|
generation_info=generation_chunk.generation_info,
|
|
)
|
|
|
|
return generation_chunk
|
|
|
|
# --- Non-streaming reasoning capture ---
|
|
|
|
def _create_chat_result(
|
|
self,
|
|
response: dict | Any,
|
|
generation_info: dict | None = None,
|
|
) -> ChatResult:
|
|
"""Extract ``reasoning`` / ``reasoning_content`` from non-streaming responses."""
|
|
result = super()._create_chat_result(response, generation_info)
|
|
response_dict = response if isinstance(response, dict) else response.model_dump()
|
|
choices = response_dict.get("choices", [])
|
|
|
|
patched_generations: list[ChatGeneration] | None = None
|
|
for index, generation in enumerate(result.generations):
|
|
choice = choices[index] if index < len(choices) else {}
|
|
choice_message = choice.get("message", {}) if isinstance(choice, Mapping) else {}
|
|
reasoning = _extract_reasoning(choice_message)
|
|
|
|
if reasoning is _MISSING and not isinstance(response, dict):
|
|
reasoning = _extract_reasoning(typed_choice_message(response, index))
|
|
|
|
message = generation.message
|
|
if reasoning is not _MISSING and isinstance(message, AIMessage):
|
|
if patched_generations is None:
|
|
patched_generations = list(result.generations)
|
|
patched_generations[index] = ChatGeneration(
|
|
message=with_reasoning_content(message, reasoning),
|
|
generation_info=generation.generation_info,
|
|
)
|
|
|
|
return ChatResult(
|
|
generations=patched_generations or result.generations,
|
|
llm_output=result.llm_output,
|
|
)
|