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
@@ -0,0 +1,333 @@
import wrapt
import time
import openai
import traceback
import aworld.trace.instrumentation.semconv as semconv
from typing import Collection, Any, Union
from aworld.trace.instrumentation import Instrumentor
from aworld.trace.base import (
Tracer,
SpanType,
get_tracer_provider_silent
)
from aworld.trace.constants import ATTRIBUTES_MESSAGE_RUN_TYPE_KEY, RunType
from aworld.trace.instrumentation.openai.inout_parse import (
run_async,
handle_openai_request,
is_streaming_response,
record_stream_response_chunk,
parse_openai_response,
record_stream_token_usage,
model_as_dict,
parse_response_message,
)
from aworld.trace.instrumentation.llm_metrics import (
record_exception_metric,
record_chat_response_metric,
record_streaming_time_to_first_token,
record_streaming_time_to_generate
)
from aworld.logs.util import logger
def _chat_wrapper(tracer: Tracer):
@wrapt.decorator
def wrapper(wrapped, instance, args, kwargs):
model_name = kwargs.get("model", "")
if not model_name:
model_name = "OpenAI"
span_attributes = {}
span_attributes[ATTRIBUTES_MESSAGE_RUN_TYPE_KEY] = RunType.LLM.value
span = tracer.start_span(
name=model_name, span_type=SpanType.CLIENT, attributes=span_attributes)
run_async(handle_openai_request(span, kwargs, instance))
start_time = time.time()
try:
response = wrapped(*args, **kwargs)
except Exception as e:
record_exception(span=span,
start_time=start_time,
exception=e
)
span.end()
raise e
if is_streaming_response(response):
return WrappedStreamResponse(span=span,
response=response,
instance=instance,
start_time=start_time,
request_kwargs=kwargs
)
record_completion(span=span,
start_time=start_time,
response=response,
request_kwargs=kwargs,
instance=instance
)
span.end()
return response
return wrapper
def _achat_class_wrapper(tracer: Tracer):
async def awrapper(wrapped, instance, args, kwargs):
model_name = kwargs.get("model", "")
if not model_name:
model_name = "OpenAI"
span_attributes = {}
span_attributes[ATTRIBUTES_MESSAGE_RUN_TYPE_KEY] = RunType.LLM.value
span = tracer.start_span(
name=model_name, span_type=SpanType.CLIENT, attributes=span_attributes)
await handle_openai_request(span, kwargs, instance)
start_time = time.time()
try:
response = await wrapped(*args, **kwargs)
except Exception as e:
record_exception(span=span,
start_time=start_time,
exception=e
)
span.end()
raise e
if is_streaming_response(response):
return WrappedStreamResponse(span=span,
response=response,
instance=instance,
start_time=start_time,
request_kwargs=kwargs
)
record_completion(span=span,
start_time=start_time,
response=response,
request_kwargs=kwargs,
instance=instance
)
span.end()
return response
return awrapper
def _achat_instance_wrapper(tracer: Tracer):
@wrapt.decorator
async def _awrapper(wrapped, instance, args, kwargs):
wrapper_func = _achat_class_wrapper(tracer)
return await wrapper_func(wrapped, instance, args, kwargs)
return _awrapper
def record_exception(span, start_time, exception):
'''
record openai chat exception to trace and metrics
'''
try:
duration = time.time() - start_time if "start_time" in locals() else 0
if span.is_recording:
span.record_exception(exception=exception)
record_exception_metric(exception=exception, duration=duration)
except Exception as e:
logger.warning(f"openai instrument record exception error.{e}")
def record_completion(span,
start_time,
response,
request_kwargs,
instance):
'''
Record chat completion to trace and metrics
'''
duration = time.time() - start_time if "start_time" in locals() else 0
response_dict = model_as_dict(response)
attributes = parse_openai_response(
response_dict, request_kwargs, instance, False)
usage = response_dict.get("usage")
choices = response_dict.get("choices")
prompt_tokens = usage.get("prompt_tokens")
completion_tokens = usage.get("completion_tokens")
span_attributes = {
**attributes,
semconv.GEN_AI_USAGE_INPUT_TOKENS: prompt_tokens,
semconv.GEN_AI_USAGE_OUTPUT_TOKENS: completion_tokens,
semconv.GEN_AI_DURATION: duration
}
span_attributes.update(parse_response_message(choices))
span.set_attributes(span_attributes)
record_chat_response_metric(attributes=attributes,
prompt_tokens=prompt_tokens,
completion_tokens=completion_tokens,
duration=duration,
choices=choices
)
class WrappedStreamResponse(wrapt.ObjectProxy):
def __init__(
self,
span,
response,
instance=None,
start_time=None,
request_kwargs=None
):
super().__init__(response)
self._span = span
self._instance = instance
self._start_time = start_time
self._complete_response = {"choices": [], "model": ""}
self._first_token_recorded = False
self._request_kwargs = request_kwargs
def __enter__(self):
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.__wrapped__.__exit__(exc_type, exc_val, exc_tb)
async def __aenter__(self):
return self
async def __aexit__(self, exc_type, exc_val, exc_tb):
await self.__wrapped__.__aexit__(exc_type, exc_val, exc_tb)
def __iter__(self):
return self
def __aiter__(self):
return self
def __next__(self):
try:
chunk = self.__wrapped__.__next__()
except Exception as e:
if isinstance(e, StopIteration):
self._close_span()
raise e
else:
self._process_stream_chunk(chunk)
return chunk
async def __anext__(self):
try:
chunk = await self.__wrapped__.__anext__()
except Exception as e:
if isinstance(e, StopAsyncIteration):
self._close_span()
raise e
else:
self._process_stream_chunk(chunk)
return chunk
def _process_stream_chunk(self, chunk):
record_stream_response_chunk(chunk, self._complete_response)
if not self._first_token_recorded:
self._time_of_first_token = time.time()
duration = self._time_of_first_token - self._start_time
attribute = parse_openai_response(
self._complete_response, self._request_kwargs, self._instance, True)
record_streaming_time_to_first_token(duration, attribute)
self._first_token_recorded = True
def _close_span(self):
duration = None
first_token_duration = None
first_token_to_generate_duration = None
if self._start_time and isinstance(self._start_time, (float, int)):
duration = time.time() - self._start_time
if self._time_of_first_token and self._start_time and isinstance(self._start_time, (float, int)):
first_token_duration = self._time_of_first_token - self._start_time
first_token_to_generate_duration = time.time() - self._time_of_first_token
prompt_usage, completion_usage = record_stream_token_usage(
self._complete_response, self._request_kwargs)
attributes = parse_openai_response(
self._complete_response, self._request_kwargs, self._instance, True)
choices = self._complete_response.get("choices")
span_attributes = {
**attributes,
"llm.prompt_tokens": prompt_usage,
"llm.completion_tokens": completion_usage,
"llm.duration": duration,
"llm.first_token_duration": first_token_duration
}
span_attributes.update(parse_response_message(choices))
self._span.set_attributes(span_attributes)
record_chat_response_metric(attributes=attributes,
prompt_tokens=prompt_usage,
completion_tokens=completion_usage,
duration=duration,
choices=choices
)
record_streaming_time_to_generate(
first_token_to_generate_duration, attributes)
self._span.end()
class OpenAIInstrumentor(Instrumentor):
def instrumentation_dependencies(self) -> Collection[str]:
return ("openai >= 1.0.0",)
def _instrument(self, **kwargs):
tracer_provider = kwargs.get("tracer_provider")
tracer = tracer_provider.get_tracer(
"aworld.trace.instrumentation.openai")
wrapt.wrap_function_wrapper(
"openai.resources.chat.completions",
"Completions.create",
_chat_wrapper(tracer=tracer)
)
wrapt.wrap_function_wrapper(
"openai.resources.chat.completions",
"AsyncCompletions.create",
_achat_class_wrapper(tracer)
)
def _instrument(self, **kwargs: Any):
pass
def wrap_openai(client: Union[openai.OpenAI, openai.AsyncOpenAI]):
"""Patch the OpenAI client to make it traceable.
Example:
client = wrap_openai(openai.OpenAI())
"""
try:
tracer_provider = get_tracer_provider_silent()
if not tracer_provider:
return
tracer = tracer_provider.get_tracer(
"aworld.trace.instrumentation.openai")
if isinstance(client, openai.OpenAI):
wrapper = _chat_wrapper(tracer)
client.chat.completions.create = wrapper(
client.chat.completions.create)
logger.info(
f"[{client.__class__}]client.chat.completions.create be warpped")
if isinstance(client, openai.AsyncOpenAI):
awrapper = _achat_instance_wrapper(tracer)
client.chat.completions.create = awrapper(
client.chat.completions.create)
logger.info(
f"[{client.__class__}]client.chat.completions.create be warpped")
except Exception:
logger.warning(traceback.format_exc())
return client
@@ -0,0 +1,296 @@
import asyncio
import os
import threading
import copy
import json
import openai
from importlib.metadata import version
from aworld.logs.util import logger
from aworld.trace.base import Span
from aworld.utils import import_package
import aworld.trace.instrumentation.semconv as semconv
_PYDANTIC_VERSION = version("pydantic")
def should_trace_prompts():
'''Determine whether it is necessary to record the message
'''
return (os.getenv("SHOULD_TRACE_PROMPTS") or "true").lower() == "true"
def need_flatten_messages():
'''Determine whether it is necessary to flatten the messages
'''
return (os.getenv("TRACE_FLATTEN_MESSAGES") or "false").lower() == "true"
def run_async(method):
try:
loop = asyncio.get_running_loop()
except RuntimeError:
loop = None
if loop and loop.is_running():
thread = threading.Thread(target=lambda: asyncio.run(method))
thread.start()
thread.join()
else:
asyncio.run(method)
async def handle_openai_request(span: Span, kwargs, instance):
if not span or not span.is_recording():
return
try:
attributes = parser_request_params(kwargs, instance)
if should_trace_prompts():
messages = kwargs.get("messages")
if need_flatten_messages():
attributes.update(parse_request_message(messages))
else:
attributes.update({
semconv.GEN_AI_PROMPT: str(messages),
})
span.set_attributes(attributes)
except ValueError as e:
logger.warning(f"trace handle openai request error: {e}")
def parser_request_params(kwargs, instance):
attributes = {
semconv.GEN_AI_SYSTEM: "OpenAI",
semconv.GEN_AI_REQUEST_MODEL: kwargs.get("model", ""),
semconv.GEN_AI_REQUEST_MAX_TOKENS: kwargs.get("max_tokens", ""),
semconv.GEN_AI_REQUEST_TEMPERATURE: kwargs.get("temperature", ""),
semconv.GEN_AI_REQUEST_TOP_P: kwargs.get("top_p", ""),
semconv.GEN_AI_REQUEST_FREQUENCY_PENALTY: kwargs.get("frequency_penalty", ""),
semconv.GEN_AI_REQUEST_PRESENCE_PENALTY: kwargs.get("presence_penalty", ""),
semconv.GEN_AI_REQUEST_USER: kwargs.get("user", ""),
semconv.GEN_AI_REQUEST_EXTRA_HEADERS: kwargs.get("extra_headers", ""),
semconv.GEN_AI_REQUEST_STREAMING: kwargs.get("stream", ""),
semconv.GEN_AI_OPERATION_NAME: "chat"
}
client = instance._client
if isinstance(client, (openai.AsyncOpenAI, openai.OpenAI)):
attributes.update({"llm.base_url": str(client.base_url)})
filterd_attri = {k: v for k, v in attributes.items()
if (v and v != "")}
return filterd_attri
def is_streaming_response(response):
return isinstance(response, openai.Stream) or isinstance(response, openai.AsyncStream)
def parse_openai_response(response, request_kwargs, instance, is_streaming):
return {
semconv.GEN_AI_RESPONSE_MODEL: response.get("model") or request_kwargs.get("model") or None,
semconv.GEN_AI_SERVER_ADDRESS: _get_openai_base_url(instance)
}
def record_stream_token_usage(complete_response, request_kwargs) -> tuple[int, int]:
'''
return (prompt_usage, completion_usage)
'''
prompt_usage = 0
completion_usage = 0
# prompt_usage
if request_kwargs and request_kwargs.get("messages"):
prompt_content = ""
model_name = complete_response.get(
"model") or request_kwargs.get("model") or "gpt-4"
for msg in request_kwargs.get("messages"):
if msg.get("content"):
prompt_content += msg.get("content")
if model_name:
prompt_usage = get_token_count_from_string(
prompt_content, model_name)
# completion_usage
if complete_response.get("choices"):
completion_content = ""
model_name = complete_response.get("model") or "gpt-4"
for choice in complete_response.get("choices"):
if choice.get("message") and choice.get("message").get("content"):
completion_content += choice["message"]["content"]
if model_name:
completion_usage = get_token_count_from_string(
completion_content, model_name)
return (prompt_usage, completion_usage)
def _get_openai_base_url(instance):
if hasattr(instance, "_client"):
client = instance._client # pylint: disable=protected-access
if isinstance(client, (openai.AsyncOpenAI, openai.OpenAI)):
return str(client.base_url)
return ""
def get_token_count_from_string(string: str, model_name: str):
import_package("tiktoken")
import tiktoken
if tiktoken_encodings.get(model_name) is None:
try:
encoding = tiktoken.encoding_for_model(model_name)
except KeyError as ex:
logger.warning(
f"Failed to get tiktoken encoding for model_name {model_name}, error: {str(ex)}")
return None
tiktoken_encodings[model_name] = encoding
else:
encoding = tiktoken_encodings.get(model_name)
token_count = len(encoding.encode(string))
return token_count
def record_stream_response_chunk(chunk, complete_response):
chunk = model_as_dict(chunk)
complete_response["model"] = chunk.get("model")
complete_response["id"] = chunk.get("id")
# prompt filter results
if chunk.get("prompt_filter_results"):
complete_response["prompt_filter_results"] = chunk.get(
"prompt_filter_results")
for choice in chunk.get("choices"):
index = choice.get("index")
if len(complete_response.get("choices")) <= index:
complete_response["choices"].append(
{"index": index, "message": {"content": "", "role": ""}})
complete_choice = complete_response.get("choices")[index]
if choice.get("finish_reason"):
complete_choice["finish_reason"] = choice.get("finish_reason")
if choice.get("content_filter_results"):
complete_choice["content_filter_results"] = choice.get(
"content_filter_results")
delta = choice.get("delta")
if delta and delta.get("content"):
complete_choice["message"]["content"] += delta.get("content")
if delta and delta.get("role"):
complete_choice["message"]["role"] = delta.get("role")
if delta and delta.get("tool_calls"):
tool_calls = delta.get("tool_calls")
if not isinstance(tool_calls, list) or len(tool_calls) == 0:
continue
if not complete_choice["message"].get("tool_calls"):
complete_choice["message"]["tool_calls"] = []
for tool_call in tool_calls:
i = int(tool_call["index"])
if len(complete_choice["message"]["tool_calls"]) <= i:
complete_choice["message"]["tool_calls"].append(
{"id": "", "function": {"name": "", "arguments": ""}}
)
span_tool_call = complete_choice["message"]["tool_calls"][i]
span_function = span_tool_call["function"]
tool_call_function = tool_call.get("function")
if tool_call.get("id"):
span_tool_call["id"] = tool_call.get("id")
if tool_call_function and tool_call_function.get("name"):
span_function["name"] = tool_call_function.get("name")
if tool_call_function and tool_call_function.get("arguments"):
span_function["arguments"] += tool_call_function.get(
"arguments")
def parse_request_message(messages):
'''
flatten request message to attributes
'''
attributes = {}
for i, msg in enumerate(messages):
prefix = f"{semconv.GEN_AI_PROMPT}.{i}"
attributes.update({f"{prefix}.role": msg.get("role")})
if msg.get("content"):
content = copy.deepcopy(msg.get("content"))
content = json.dumps(content)
attributes.update({f"{prefix}.content": content})
if msg.get("tool_call_id"):
attributes.update({
f"{prefix}.tool_call_id": msg.get("tool_call_id")})
tool_calls = msg.get("tool_calls")
if tool_calls:
for i, tool_call in enumerate(tool_calls):
tool_call = model_as_dict(tool_call)
function = tool_call.get("function")
attributes.update({
f"{prefix}.tool_calls.{i}.id": tool_call.get("id")})
attributes.update({
f"{prefix}.tool_calls.{i}.name": function.get("name")})
attributes.update({
f"{prefix}.tool_calls.{i}.arguments": function.get("arguments")})
return attributes
def parse_response_message(choices) -> dict:
attributes = {}
if not should_trace_prompts():
return attributes
for choice in choices:
index = choice.get("index")
prefix = f"{semconv.GEN_AI_COMPLETION}.{index}"
attributes.update(
{f"{prefix}.finish_reason": choice.get("finish_reason")})
message = choice.get("message")
if not message:
continue
attributes.update({f"{prefix}.role": message.get("role")})
if message.get("refusal"):
attributes.update({f"{prefix}.refusal": message.get("refusal")})
else:
attributes.update({f"{prefix}.content": message.get("content")})
function_call = message.get("function_call")
if function_call:
attributes.update(
{f"{prefix}.tool_calls.0.name": function_call.get("name")})
attributes.update(
{f"{prefix}.tool_calls.0.arguments": function_call.get("arguments")})
tool_calls = message.get("tool_calls")
if tool_calls:
for i, tool_call in enumerate(tool_calls):
function = tool_call.get("function")
attributes.update(
{f"{prefix}.tool_calls.{i}.id": tool_call.get("id")})
attributes.update(
{f"{prefix}.tool_calls.{i}.name": function.get("name")})
attributes.update(
{f"{prefix}.tool_calls.{i}.arguments": function.get("arguments")})
return attributes
def model_as_dict(model):
if isinstance(model, dict):
return model
if _PYDANTIC_VERSION < "2.0.0":
return model.dict()
if hasattr(model, "model_dump"):
return model.model_dump()
elif hasattr(model, "parse"): # Raw API response
return model_as_dict(model.parse())
else:
return model