ai-agent-book 精选快照(<2MB 代码与文档,来自 github.com/bojieli/ai-agent-book)
Build latest book artifacts / build (push) Canceled after 0s
dependency resolution / resolve (3.11) (push) Canceled after 0s
dependency resolution / resolve (3.13) (push) Canceled after 0s
deploy-pages / build (push) Canceled after 0s
deploy-pages / deploy (push) Canceled after 0s
i18n consistency check / check (push) Canceled after 0s
provider adoption tests / test (chapter2/context-compression) (push) Canceled after 0s
provider adoption tests / test (chapter2/prompt-injection) (push) Canceled after 0s
provider adoption tests / test (chapter2/system-hint) (push) Canceled after 0s
provider adoption tests / test (chapter3/log-sanitization) (push) Canceled after 0s
web-search-agent tests / test (push) Canceled after 0s
web-search-agent tests / agentbook (push) Canceled after 0s

This commit is contained in:
2026-08-20 13:12:50 +00:00
commit b119135836
10275 changed files with 3284984 additions and 0 deletions
+396
View File
@@ -0,0 +1,396 @@
"""Exact GPT-5.6 Responses API agent for Experiment 1-3.
The previous companion sent Responses-style hosted tools to Chat Completions
through a proxy and then reported an empty ``tool_calls`` list. This module
uses the actual ``/v1/responses`` protocol and preserves its typed output items
(``web_search_call``, ``code_interpreter_call``, messages, and citations).
"""
from __future__ import annotations
import json
import logging
import time
from typing import Any, Dict, List, Literal, Optional
import requests
logger = logging.getLogger(__name__)
class GPT5NativeAgent:
"""GPT-5.6 Sol with OpenAI-hosted web search and Python tools."""
def __init__(
self,
api_key: str,
base_url: str = "https://api.openai.com/v1",
model: str = "gpt-5.6-sol",
):
if not api_key:
raise ValueError("An API key is required")
self.api_key = api_key
self.base_url = base_url.rstrip("/")
self.model = model
self.provider = (
"openai" if self.base_url == "https://api.openai.com/v1" else
"openrouter" if "openrouter.ai" in self.base_url else
"dashscope" if "dashscope" in self.base_url else
"custom"
)
self.conversation_history: List[Dict[str, Any]] = []
self.system_prompt = self._create_system_prompt()
self.previous_response_id: Optional[str] = None
self.api_turns: List[Dict[str, Any]] = []
@staticmethod
def _create_system_prompt() -> str:
return """You are a deep-research assistant. 你是一名深度研究助手。
Hard rule / 硬性规则: when the user's research request leaves material
preferences ambiguous — for example which data source to use or which
technical indicators to compute — ask a concise clarifying question FIRST
(for example “您偏好使用哪个数据源?需要分析哪些技术指标?”), and do NOT
call any tool until the user answers.
当用户的研究请求没有明确数据来源或具体分析指标时,必须先向用户提问澄清,
在用户回答之前不要调用任何工具。
After clarification, use hosted web search for current facts and cite
sources, and use the hosted Python/code-interpreter tool for quantitative
analysis; do not claim a calculation was run unless the response contains a
completed code_interpreter_call.
澄清之后:使用 web_search 获取最新事实并引用来源链接;所有定量计算必须通过
code_interpreter 实际执行,不得口算或声称运行了代码。"""
def _tools(self) -> List[Dict[str, Any]]:
if self.provider == "dashscope":
# Exact structures from the Alibaba Model Studio Responses API guides.
return [{"type": "web_search"}, {"type": "code_interpreter"}]
# Exact structures from the official OpenAI Responses API guides.
return [
{"type": "web_search", "search_context_size": "medium"},
{
"type": "code_interpreter",
"container": {"type": "auto", "memory_limit": "4g"},
},
]
def _build_responses_request(
self,
input_text: str,
*,
use_tools: bool = True,
tool_choice: Literal["auto", "none", "required"] = "auto",
reasoning_effort: str = "low",
verbosity: Optional[str] = None,
max_output_tokens: Optional[int] = None,
background: bool = False,
) -> Dict[str, Any]:
if self.provider != "dashscope":
if reasoning_effort not in {"none", "low", "medium", "high", "xhigh", "max"}:
raise ValueError("Unsupported GPT-5.6 reasoning effort")
if verbosity not in {None, "low", "medium", "high"}:
raise ValueError("verbosity must be low, medium, or high")
request: Dict[str, Any] = {
"model": self.model,
"instructions": self.system_prompt,
"input": input_text,
}
if self.provider == "dashscope":
# DashScope runs thinking natively and has no reasoning.effort or
# text.verbosity knobs; its gateway also drops non-streaming
# requests that stay silent for ~60s, so streaming is mandatory.
request["stream"] = True
else:
request["reasoning"] = {"effort": reasoning_effort}
request["background"] = background
request["store"] = True
if verbosity:
request["text"] = {"verbosity": verbosity}
if max_output_tokens:
request["max_output_tokens"] = max_output_tokens
if use_tools:
request["tools"] = self._tools()
request["tool_choice"] = tool_choice
if self.previous_response_id:
request["previous_response_id"] = self.previous_response_id
return request
@staticmethod
def _output_text(response: Dict[str, Any]) -> str:
if not isinstance(response, dict):
return ""
chunks: List[str] = []
for item in response.get("output") or []:
if not isinstance(item, dict) or item.get("type") != "message":
continue
for content in item.get("content") or []:
if isinstance(content, dict) and content.get("type") == "output_text" and content.get("text"):
chunks.append(content["text"])
return "\n".join(chunks).strip()
@staticmethod
def _tool_items(response: Dict[str, Any]) -> List[Dict[str, Any]]:
if not isinstance(response, dict):
return []
return [
item
for item in response.get("output") or []
if isinstance(item, dict)
and item.get("type") in {
"web_search_call",
"code_interpreter_call",
"hosted_tool_call",
}
]
@staticmethod
def _citations(response: Dict[str, Any]) -> List[Dict[str, Any]]:
citations = []
if not isinstance(response, dict):
return citations
for item in response.get("output") or []:
if not isinstance(item, dict):
continue
for content in item.get("content") or []:
if not isinstance(content, dict):
continue
for annotation in content.get("annotations") or []:
if isinstance(annotation, dict) and annotation.get("type") in {
"url_citation",
"container_file_citation",
}:
citations.append(annotation)
# DashScope reports sources on the web_search_call item itself
# instead of url_citation annotations; normalize them here.
if item.get("type") == "web_search_call":
action = item.get("action")
if isinstance(action, dict):
for source in action.get("sources") or []:
url = source if isinstance(source, str) else (source.get("url") if isinstance(source, dict) else None)
if url:
citations.append(
{"type": "url_citation", "url": url}
)
return citations
def _post_responses(
self, request: Dict[str, Any]
) -> tuple[int, Dict[str, Any], Optional[Dict[str, int]]]:
"""Send one Responses request and return (status, body, stream_events).
DashScope requires streaming; the final ``response.completed`` event
carries the same response object the non-streaming API returns, so both
paths converge on an identical shape.
"""
headers = {
"Authorization": f"Bearer {self.api_key}",
"Content-Type": "application/json",
}
if not request.get("stream"):
http_response = requests.post(
f"{self.base_url}/responses",
headers=headers,
json=request,
timeout=900,
)
try:
return http_response.status_code, http_response.json(), None
except ValueError:
return http_response.status_code, {"raw_text": http_response.text}, None
event_counts: Dict[str, int] = {}
final_response: Optional[Dict[str, Any]] = None
with requests.post(
f"{self.base_url}/responses",
headers=headers,
json=request,
stream=True,
timeout=900,
) as http_response:
status_code = http_response.status_code
if not http_response.ok:
return status_code, {"raw_text": http_response.text}, event_counts
for line in http_response.iter_lines(decode_unicode=True):
if not line or not line.startswith("data:"):
continue
data = line[len("data:"):].strip()
if data == "[DONE]":
break
try:
event = json.loads(data)
except ValueError:
continue
event_type = event.get("type") or "unknown"
event_counts[event_type] = event_counts.get(event_type, 0) + 1
if event_type in {"response.completed", "response.failed"}:
final_response = event.get("response")
if final_response is None:
return status_code, {"error": {"type": "stream_incomplete",
"message": "stream ended without response.completed"}}, event_counts
return status_code, final_response, event_counts
def process_request(
self,
user_request: str,
use_tools: bool = True,
tool_choice: Literal["auto", "none", "required"] = "auto",
temperature: float = 0.3,
max_tokens: Optional[int] = None,
reasoning_effort: str = "low",
verbosity: Optional[str] = None,
dry_run: bool = False,
background: bool = False,
) -> Dict[str, Any]:
"""Create one Responses API turn and retain its complete trace.
``temperature`` remains in the signature for legacy callers, but is not
sent: GPT-5.6 reasoning requests use ``reasoning.effort`` instead.
"""
request = self._build_responses_request(
user_request,
use_tools=use_tools,
tool_choice=tool_choice,
reasoning_effort=reasoning_effort,
verbosity=verbosity,
max_output_tokens=max_tokens,
background=background,
)
if dry_run:
return {
"success": True,
"dry_run": True,
"request": request,
"response": None,
"tool_calls": [],
"model": self.model,
"provider": self.provider,
}
started = time.monotonic()
try:
status_code, response, stream_events = self._post_responses(request)
elapsed = round(time.monotonic() - started, 6)
turn = {
"request": json.loads(json.dumps(request, ensure_ascii=False)),
"http_status": status_code,
"response": response,
"elapsed_seconds": elapsed,
}
if stream_events:
turn["stream_event_counts"] = stream_events
self.api_turns.append(turn)
if not isinstance(response, dict) or status_code >= 400 or response.get("error"):
error = (response.get("error") if isinstance(response, dict) else None) or {
"type": "http_error",
"message": (response.get("raw_text") if isinstance(response, dict) else None) or (json.dumps(response)[:500] if response is not None else "Empty response"),
}
return {
"success": False,
"error": error,
"response": None,
"request": request,
"raw_response": response,
"tool_calls": [],
"citations": [],
"usage": (response.get("usage") if isinstance(response, dict) else {}) or {},
"model": self.model,
"provider": self.provider,
"base_url": self.base_url,
"elapsed_seconds": elapsed,
}
self.previous_response_id = response.get("id")
text = self._output_text(response)
self.conversation_history.extend(
[
{"role": "user", "content": user_request},
{"role": "assistant", "content": text},
]
)
return {
"success": response.get("status") == "completed" and bool(text),
"error": response.get("error"),
"response": text,
"request": request,
"raw_response": response,
"output_items": response.get("output") or [],
"tool_calls": self._tool_items(response),
"citations": self._citations(response),
"usage": response.get("usage") or {},
"model": response.get("model") or self.model,
"requested_model": self.model,
"provider": self.provider,
"base_url": self.base_url,
"response_id": response.get("id"),
"status": response.get("status"),
"elapsed_seconds": elapsed,
"temperature_omitted_for_reasoning_model": temperature is not None,
}
except Exception as exc:
elapsed = round(time.monotonic() - started, 6)
self.api_turns.append(
{
"request": request,
"elapsed_seconds": elapsed,
"error": {"class": type(exc).__name__, "message": str(exc)},
}
)
return {
"success": False,
"error": {"class": type(exc).__name__, "message": str(exc)},
"response": None,
"request": request,
"tool_calls": [],
"citations": [],
"model": self.model,
"provider": self.provider,
"base_url": self.base_url,
"elapsed_seconds": elapsed,
}
def search_and_analyze(
self, topic: str, analysis_code: Optional[str] = None
) -> Dict[str, Any]:
code_requirement = (
f"Run this supplied Python in the hosted tool and inspect its output:\n{analysis_code}"
if analysis_code
else "Use the hosted Python tool for all quantitative processing."
)
return self.process_request(
f"Research current information about {topic}. {code_requirement} "
"Cite web sources and distinguish searched facts from computed results.",
use_tools=True,
reasoning_effort="medium",
)
def clear_history(self) -> None:
self.conversation_history = []
self.previous_response_id = None
self.api_turns = []
def get_history(self) -> List[Dict[str, Any]]:
return json.loads(json.dumps(self.conversation_history, ensure_ascii=False))
def set_system_prompt(self, prompt: str) -> None:
self.system_prompt = prompt
class GPT5AgentChain:
"""Sequential Responses turns linked with ``previous_response_id``."""
def __init__(self, agent: GPT5NativeAgent):
self.agent = agent
self.chain_results: List[Dict[str, Any]] = []
def add_step(self, request: str, **kwargs: Any) -> "GPT5AgentChain":
self.chain_results.append(
{"request": request, "result": self.agent.process_request(request, **kwargs)}
)
return self
def execute(self) -> List[Dict[str, Any]]:
return self.chain_results
def clear(self) -> None:
self.chain_results = []