diff --git a/docs/en/docs/how-to/trace-with-phoenix.md b/docs/en/docs/how-to/trace-with-phoenix.md index 9e1d1abd6..c51462260 100644 --- a/docs/en/docs/how-to/trace-with-phoenix.md +++ b/docs/en/docs/how-to/trace-with-phoenix.md @@ -91,12 +91,6 @@ PowerContext configures inference instrumentation to exclude content. Spans carr durations, and error categories. Prompts, model responses, Memory content, and vectors are excluded, and message attributes record only the shape of each message rather than its text. -One exception applies to generation. When a model returns output that does not satisfy the requested schema, -Pydantic AI retries with feedback that quotes the model's own invalid output, and it records that feedback in the -`gen_ai.input.messages` and `pydantic_ai.all_messages` attributes regardless of the content setting. For Memory -extraction, that quoted output is the proposed Memory content. Treat the tracing backend as a system that may receive -model output on this retry path, and restrict access to it accordingly. - ## Stop Phoenix ```bash diff --git a/docs/en/docs/reference/configuration.md b/docs/en/docs/reference/configuration.md index bcbd25508..47deb2a96 100644 --- a/docs/en/docs/reference/configuration.md +++ b/docs/en/docs/reference/configuration.md @@ -143,7 +143,7 @@ that do not use the `powercontext` command may omit the `cli` extra. Enabling tracing also produces spans for the generation and embedding calls that PowerContext constructs, without recording prompts, model responses, Memory content, or vectors. See -[Trace with Phoenix](../how-to/trace-with-phoenix.md) for a working configuration and the one documented exception. +[Trace with Phoenix](../how-to/trace-with-phoenix.md) for a working configuration. To use OceanBase, provide its URL through your environment or secret manager: diff --git a/docs/zh/docs/how-to/trace-with-phoenix.md b/docs/zh/docs/how-to/trace-with-phoenix.md index 7ac587378..14d303bdb 100644 --- a/docs/zh/docs/how-to/trace-with-phoenix.md +++ b/docs/zh/docs/how-to/trace-with-phoenix.md @@ -87,11 +87,6 @@ span 是批量导出的,刷新前请稍等几秒。MCP 请求会用 `MCP mcp.t PowerContext 在配置推理 instrumentation 时关闭了内容记录。span 只携带模型标识、token 用量、耗时和错误类别; prompt、模型响应、Memory 内容和向量都不会被导出,消息类属性只记录每条消息的结构,不记录正文。 -generation 有一个例外。当模型返回的输出不满足所要求的 schema 时,Pydantic AI 会带着反馈重试,而这段反馈里 -引用了模型自己的非法输出;无论内容开关如何设置,它都会被写入 `gen_ai.input.messages` 和 -`pydantic_ai.all_messages` 属性。对 Memory extraction 来说,被引用的那段输出就是候选的 Memory 内容。 -因此应把 tracing 后端视为在这条重试路径上可能收到模型输出的系统,并相应限制其访问权限。 - ## 停止 Phoenix ```bash diff --git a/docs/zh/docs/reference/configuration.md b/docs/zh/docs/reference/configuration.md index 9c12749a8..ddf9b9f68 100644 --- a/docs/zh/docs/reference/configuration.md +++ b/docs/zh/docs/reference/configuration.md @@ -138,8 +138,7 @@ OpenTelemetry 环境变量进行配置。不使用 `powercontext` command 的 pr `cli` extra。 启用 tracing 后,PowerContext 自己构造的 generation 与 embedding 调用也会产生 span,且不记录 prompt、模型响应、 -Memory 内容或向量。可运行的配置和唯一一处已记录的例外见 -[用 Phoenix 查看 trace](../how-to/trace-with-phoenix.md)。 +Memory 内容或向量。可运行的配置见 [用 Phoenix 查看 trace](../how-to/trace-with-phoenix.md)。 使用 OceanBase 时,通过环境或 secret manager 提供 URL: diff --git a/pyproject.toml b/pyproject.toml index e37d57aa5..76a64549e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,7 +25,7 @@ classifiers = [ builtin = [ "aiosqlite>=0.22,<1", "apscheduler>=3.11,<4", - "pydantic-ai-slim[anthropic,openai]>=2.14.1,<3", + "pydantic-ai-slim[anthropic,openai]>=2.27.1,<3", "pydantic-settings>=2.7,<3", "pyobvector>=0.2.28,<0.3", "sqlalchemy[asyncio]>=2,<3", diff --git a/tests/builtin/inference/test_pydantic_ai.py b/tests/builtin/inference/test_pydantic_ai.py index 0ee130d8f..ea47d88d0 100644 --- a/tests/builtin/inference/test_pydantic_ai.py +++ b/tests/builtin/inference/test_pydantic_ai.py @@ -23,7 +23,7 @@ from pydantic_ai.exceptions import ModelHTTPError from pydantic_ai.messages import ModelMessage, ModelResponse, TextPart from pydantic_ai.models.function import AgentInfo, FunctionModel -from pydantic_ai.models.instrumented import InstrumentationSettings +from pydantic_ai.models.instrumented import InstrumentationSettings, InstrumentedModel from pydantic_ai.models.test import TestModel from pydantic_ai.usage import RequestUsage @@ -52,6 +52,19 @@ class Answer: value: str +# Nested on purpose: retry feedback only carries the raw model output when the validation +# error location is longer than one element, which a flat output type cannot produce. +@dataclass(frozen=True, slots=True) +class Candidate: + text: str + intent: str + + +@dataclass(frozen=True, slots=True) +class Proposal: + candidates: tuple[Candidate, ...] + + TEST_PROFILE = EmbeddingProfile( profile_id="test-v1", model="test:test", @@ -271,6 +284,48 @@ async def scenario() -> None: asyncio.run(scenario()) +def test_instrumented_generation_spans_exclude_schema_retry_content() -> None: + exporter = InMemorySpanExporter() + provider = TracerProvider(shutdown_on_exit=False) + provider.add_span_processor(SimpleSpanProcessor(exporter)) + instrumentation = InstrumentationSettings( + tracer_provider=provider, + include_content=False, + include_binary_content=False, + include_model_request_parameters=False, + ) + # The first response drops the required `intent`, so the retry feedback quotes it back. + pending = [ + '{"candidates":[{"text":"traveler prefers aisle seats"}]}', + '{"candidates":[{"text":"redacted","intent":"add"}]}', + ] + + async def reply(messages: list[ModelMessage], info: AgentInfo) -> ModelResponse: + del messages, info + return ModelResponse(parts=[TextPart(pending.pop(0))]) + + async def scenario() -> None: + generator = PydanticAIStructuredGenerator( + model=InstrumentedModel(FunctionModel(reply), instrumentation), + instructions="Propose candidates.", + input_type=Question, + output_type=Proposal, + ) + + result = await generator.generate(Question("bounded evidence")) + + assert result.output.candidates[0].intent == "add" + + asyncio.run(scenario()) + + spans = exporter.get_finished_spans() + # Both responses consumed and two chat spans recorded prove the retry path ran. + assert not pending + assert len([span for span in spans if span.name.startswith("chat ")]) == 2 + for span in spans: + assert "traveler prefers aisle seats" not in str(span.attributes) + + def test_embedding_adapter_returns_validated_vectors_and_usage() -> None: async def scenario() -> None: model = PydanticAIEmbeddingModel( diff --git a/uv.lock b/uv.lock index 68becedeb..e6ef29590 100644 --- a/uv.lock +++ b/uv.lock @@ -763,15 +763,15 @@ wheels = [ [[package]] name = "genai-prices" -version = "0.0.71" +version = "0.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx2" }, { name = "pydantic" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/1d/e4/5072862613fba039da2b7c981a8649c6c6bbcb2863bd8bc81617c09ce5ee/genai_prices-0.0.71.tar.gz", hash = "sha256:de4db34ec38404f9ef383cb1ab29e204d16ccf27071af0b16d5747ee7affe36b", size = 82105, upload-time = "2026-07-10T00:38:30.491Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/e1/203175825018a5dcdc5fde62afae57c0aa844ff1584d6c8d148107623f59/genai_prices-0.1.2.tar.gz", hash = "sha256:930ffd34e3b65e32d24818073a2fb65f0174c4de7f6afbebfee1efdf8b003877", size = 92403, upload-time = "2026-08-12T17:20:33.468Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/98/c06c1318f6834a26268a2d4280e4183f60c5ea92152aea841feff29826ff/genai_prices-0.0.71-py3-none-any.whl", hash = "sha256:1d13111563af2b1ce43ccfacf77b7ac3216ad704c644408a56e11b181fe0d128", size = 84586, upload-time = "2026-07-10T00:38:29.252Z" }, + { url = "https://files.pythonhosted.org/packages/a8/aa/54f2e6ed2699889fb5b062b1a6a58afa46f9fd0d095dc183e2173bd0b415/genai_prices-0.1.2-py3-none-any.whl", hash = "sha256:bdcdb9b358de01d9c3d4337a86b8dbfbf4e8044c2c064e6e3c4a6fef2f640382", size = 96579, upload-time = "2026-08-12T17:20:32.159Z" }, ] [[package]] @@ -1875,7 +1875,7 @@ requires-dist = [ { name = "powercontext", extras = ["client"], marker = "extra == 'cli'" }, { name = "prometheus-client", marker = "extra == 'server'", specifier = ">=0.21,<1" }, { name = "pydantic", specifier = ">=2.10,<3" }, - { name = "pydantic-ai-slim", extras = ["anthropic", "openai"], marker = "extra == 'builtin'", specifier = ">=2.14.1,<3" }, + { name = "pydantic-ai-slim", extras = ["anthropic", "openai"], marker = "extra == 'builtin'", specifier = ">=2.27.1,<3" }, { name = "pydantic-settings", marker = "extra == 'builtin'", specifier = ">=2.7,<3" }, { name = "pydantic-settings", marker = "extra == 'client'", specifier = ">=2.7,<3" }, { name = "pydantic-settings", marker = "extra == 'server'", specifier = ">=2.7,<3" }, @@ -2007,9 +2007,10 @@ email = [ [[package]] name = "pydantic-ai-slim" -version = "2.14.1" +version = "2.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "anyio" }, { name = "genai-prices" }, { name = "griffelib" }, { name = "httpx" }, @@ -2018,9 +2019,9 @@ dependencies = [ { name = "pydantic-graph" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/20/0e/33989e7bcd2abaefdc2534129055dce0943bbcdcc5212d2296f79fd604b7/pydantic_ai_slim-2.14.1.tar.gz", hash = "sha256:3ce0afde81cd50fcd434ccdd46774f4386bee8cc0ed3e75d7dc8784fdca303ff", size = 866335, upload-time = "2026-07-21T05:40:59.91Z" } +sdist = { url = "https://files.pythonhosted.org/packages/6d/47/9ea38824b27efae7d19a34e2288697e0a84aa5cdd8c119ac85d030395ea3/pydantic_ai_slim-2.29.0.tar.gz", hash = "sha256:a5de25ebf8cf50187fe71be101e318e18e8ed469e5d8d1551d92b81ded1e3e99", size = 1197221, upload-time = "2026-08-13T04:16:29.027Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/74/51/8682b564598571d7056971d62221fafda2ce513481cc241f3ac621e6460e/pydantic_ai_slim-2.14.1-py3-none-any.whl", hash = "sha256:64687be32deb8075eb99edba837a61d22daf1801cd3b1e6925e8519c13838934", size = 1050646, upload-time = "2026-07-21T05:40:52.205Z" }, + { url = "https://files.pythonhosted.org/packages/a1/42/fda259cd11f88eb0ce7e54ee6a29011c0dd6a0af474403a2bb32d5dddb79/pydantic_ai_slim-2.29.0-py3-none-any.whl", hash = "sha256:cb5f5b7a941715b7f8894baae48ac178a603bdcf729726535964eaec25b07eb1", size = 1414034, upload-time = "2026-08-13T04:16:22.19Z" }, ] [package.optional-dependencies] @@ -2136,17 +2137,18 @@ wheels = [ [[package]] name = "pydantic-graph" -version = "2.14.1" +version = "2.29.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "anyio" }, { name = "httpx" }, { name = "logfire-api" }, { name = "pydantic" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e8/e7/db61f8c7a323feb55884ca701f138ae18911126628595375ff9d0d47397a/pydantic_graph-2.14.1.tar.gz", hash = "sha256:8f3a577a3ae278012d168c0db07a9bdf1b0021d21e48831c0b05518f2230730e", size = 43934, upload-time = "2026-07-21T05:41:02.136Z" } +sdist = { url = "https://files.pythonhosted.org/packages/99/a4/364964ff4ca90fac651ffc969e04d75941389a39009fa2e950379bbcec93/pydantic_graph-2.29.0.tar.gz", hash = "sha256:078d2d7ab53ffb695e1a66c2a741880c88fef36611004736bf7313f8a5ec4465", size = 45180, upload-time = "2026-08-13T04:16:30.94Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/26/03/ff2ee9174ea0083638c1e31b5f5c92ffd3e795024db23caa0ea297458cd5/pydantic_graph-2.14.1-py3-none-any.whl", hash = "sha256:4e4b2afb0751b5495fce9f253a5b4ccac9a0f86989fa7479ee8d517f76bd449f", size = 51658, upload-time = "2026-07-21T05:40:55.711Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d5/9a7e31a4895904c5d81989069630c47b7ef28d07256ae7ce1b8cda9964d1/pydantic_graph-2.29.0-py3-none-any.whl", hash = "sha256:afc81688c9abca9e93c4f7b81dd1112e8a7a8d4d41514413bd8d2c674bd2dfa1", size = 52661, upload-time = "2026-08-13T04:16:24.994Z" }, ] [[package]]