Ajustes conforme relatorio de testes 2026-08-27
This commit is contained in:
@@ -145,8 +145,17 @@ class GuardrailLLMClient:
|
||||
except RuntimeError:
|
||||
return asyncio.run(_call())
|
||||
|
||||
# ``ContextVar`` values do not cross ThreadPoolExecutor boundaries by
|
||||
# default. Preserve the framework request/trace/parent observation when
|
||||
# this legacy sync bridge needs a worker thread; otherwise a provider
|
||||
# created inside the worker sees no active correlation context and the
|
||||
# optional langfuse.openai wrapper may emit a standalone OpenAI-generation
|
||||
# trace.
|
||||
from contextvars import copy_context
|
||||
|
||||
context = copy_context()
|
||||
with ThreadPoolExecutor(max_workers=1, thread_name_prefix="guardrail-compat") as executor:
|
||||
return executor.submit(lambda: asyncio.run(_call())).result()
|
||||
return executor.submit(context.run, lambda: asyncio.run(_call())).result()
|
||||
|
||||
def classify(
|
||||
self,
|
||||
|
||||
@@ -58,6 +58,55 @@ def _coerce_reasoning_text(value: Any) -> str | None:
|
||||
return text or None
|
||||
|
||||
|
||||
def _coerce_message_content(value: Any) -> str:
|
||||
"""Normalize OpenAI-compatible message content without using reasoning as answer.
|
||||
|
||||
OpenAI-compatible implementations may expose ``message.content`` as a plain
|
||||
string, a list of content parts, or SDK objects/dicts containing ``text``.
|
||||
Unknown shapes fail closed to an empty string instead of serializing the raw
|
||||
response object into the assistant answer.
|
||||
"""
|
||||
if value is None:
|
||||
return ""
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
if isinstance(value, (list, tuple)):
|
||||
chunks: list[str] = []
|
||||
for item in value:
|
||||
if isinstance(item, str):
|
||||
chunks.append(item)
|
||||
continue
|
||||
if isinstance(item, dict):
|
||||
text = item.get("text")
|
||||
if isinstance(text, str):
|
||||
chunks.append(text)
|
||||
continue
|
||||
text = getattr(item, "text", None)
|
||||
if isinstance(text, str):
|
||||
chunks.append(text)
|
||||
return "".join(chunks)
|
||||
if isinstance(value, dict):
|
||||
text = value.get("text")
|
||||
return text if isinstance(text, str) else ""
|
||||
text = getattr(value, "text", None)
|
||||
return text if isinstance(text, str) else ""
|
||||
|
||||
|
||||
def _extract_openai_message_content(message: Any) -> str:
|
||||
if message is None:
|
||||
return ""
|
||||
if isinstance(message, dict):
|
||||
return _coerce_message_content(message.get("content"))
|
||||
return _coerce_message_content(getattr(message, "content", None))
|
||||
|
||||
|
||||
def _extract_finish_reason(choice: Any) -> str | None:
|
||||
if choice is None:
|
||||
return None
|
||||
value = choice.get("finish_reason") if isinstance(choice, dict) else getattr(choice, "finish_reason", None)
|
||||
return str(value) if value is not None else None
|
||||
|
||||
|
||||
def _extract_reasoning_content(obj: Any) -> str | None:
|
||||
"""Best-effort extraction across OpenAI-compatible and OCI response shapes."""
|
||||
if obj is None:
|
||||
@@ -272,10 +321,27 @@ class OCICompatibleOpenAIProvider(LLMProvider):
|
||||
getattr(settings, "ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION", None)
|
||||
or os.getenv("ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION", "false")
|
||||
).strip().lower() in {"1", "true", "yes", "on", "y"}
|
||||
if self.telemetry is not None and use_langfuse_wrapper:
|
||||
|
||||
# The framework owns Langfuse correlation. Even compatibility paths may
|
||||
# instantiate a provider without passing ``Telemetry`` explicitly while a
|
||||
# request is already active (for example GuardrailLLMClient running in its
|
||||
# sync bridge). In that situation langfuse.openai would auto-create an
|
||||
# ``OpenAI-generation`` root trace instead of attaching to the business
|
||||
# request. Treat an active framework observability context exactly like
|
||||
# an injected Telemetry instance and keep the standard OpenAI client.
|
||||
active_framework_trace = False
|
||||
try:
|
||||
from agent_framework.observability.context import get_observability_context
|
||||
|
||||
obs_ctx = get_observability_context()
|
||||
active_framework_trace = bool(obs_ctx.trace_id or obs_ctx.request_id)
|
||||
except Exception:
|
||||
active_framework_trace = False
|
||||
|
||||
if use_langfuse_wrapper and (self.telemetry is not None or active_framework_trace):
|
||||
logger.warning(
|
||||
"ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true ignorado porque o provider já recebeu "
|
||||
"Telemetry do framework; instrumentação dupla pode criar observations fora do contrato de mapping."
|
||||
"ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true ignorado durante execução correlacionada "
|
||||
"do framework; langfuse.openai pode criar OpenAI-generation como trace raiz separado."
|
||||
)
|
||||
use_langfuse_wrapper = False
|
||||
if getattr(settings, "ENABLE_LANGFUSE", False) and use_langfuse_wrapper:
|
||||
@@ -432,9 +498,29 @@ class OCICompatibleOpenAIProvider(LLMProvider):
|
||||
model_parameters=model_parameters,
|
||||
) as generation:
|
||||
resp = await client.chat.completions.create(**request_kwargs)
|
||||
message = resp.choices[0].message
|
||||
answer = message.content or ""
|
||||
reasoning_content = _extract_reasoning_content(message)
|
||||
choices = getattr(resp, "choices", None) or []
|
||||
if not choices:
|
||||
message = None
|
||||
answer = ""
|
||||
reasoning_content = None
|
||||
finish_reason = None
|
||||
logger.warning(
|
||||
"OpenAI-compatible LLM returned no choices provider=%s model=%s profile=%s component=%s",
|
||||
provider, model, resolved_profile_name, component_name,
|
||||
)
|
||||
else:
|
||||
choice = choices[0]
|
||||
message = choice.get("message") if isinstance(choice, dict) else getattr(choice, "message", None)
|
||||
answer = _extract_openai_message_content(message)
|
||||
reasoning_content = _extract_reasoning_content(message)
|
||||
finish_reason = _extract_finish_reason(choice)
|
||||
|
||||
logger.info(
|
||||
"OpenAI-compatible LLM response provider=%s model=%s profile=%s component=%s "
|
||||
"finish_reason=%s content_len=%d reasoning_len=%d",
|
||||
provider, model, resolved_profile_name, component_name,
|
||||
finish_reason, len(answer), len(reasoning_content or ""),
|
||||
)
|
||||
|
||||
usage_metadata = self.token_collector.enrich(model, getattr(resp, "usage", None))
|
||||
usage_metadata.update({
|
||||
@@ -445,8 +531,16 @@ class OCICompatibleOpenAIProvider(LLMProvider):
|
||||
"component": component_name,
|
||||
"model": model,
|
||||
"provider": provider,
|
||||
"finish_reason": finish_reason,
|
||||
"content_length": len(answer),
|
||||
"reasoning_content_length": len(reasoning_content or ""),
|
||||
**model_parameters,
|
||||
})
|
||||
llm_metadata.update({
|
||||
"finish_reason": finish_reason,
|
||||
"content_length": len(answer),
|
||||
"reasoning_content_length": len(reasoning_content or ""),
|
||||
})
|
||||
generation.set_output(answer)
|
||||
generation.set_usage(usage_metadata)
|
||||
generation.set_metadata(**usage_metadata)
|
||||
|
||||
@@ -47,13 +47,13 @@ internal steps = observations/spans/generations inside that trace
|
||||
|
||||
- Langfuse OpenAI auto-instrumentation is now opt-in.
|
||||
- Default behavior uses the standard `openai.AsyncOpenAI` client and relies on the framework's own `Telemetry.generation(...)` to create correlated Langfuse generations.
|
||||
- To re-enable wrapper-based auto-instrumentation, set:
|
||||
- The supported framework default is:
|
||||
|
||||
```env
|
||||
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true
|
||||
ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=false
|
||||
```
|
||||
|
||||
For this framework, the recommended default is to keep it disabled.
|
||||
All `.env.example` files in the repository declare this value explicitly. Set it to `true` only for isolated compatibility/diagnostic deployments that intentionally need to capture OpenAI SDK calls outside the framework telemetry path.
|
||||
|
||||
## Expected result
|
||||
|
||||
@@ -86,7 +86,7 @@ framework_judges
|
||||
Run the backend and execute one request. Then verify:
|
||||
|
||||
1. The `Traces` screen has one trace row for the request, not one row per node.
|
||||
2. `OpenAI-generation` no longer appears as a separate top-level trace unless `ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true`.
|
||||
2. `OpenAI-generation` no longer appears as a separate top-level trace with the supported default `ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=false`.
|
||||
3. LangGraph node events and IC/NOC/GRL events appear under the same request trace.
|
||||
|
||||
|
||||
|
||||
@@ -145,8 +145,17 @@ class GuardrailLLMClient:
|
||||
except RuntimeError:
|
||||
return asyncio.run(_call())
|
||||
|
||||
# ``ContextVar`` values do not cross ThreadPoolExecutor boundaries by
|
||||
# default. Preserve the framework request/trace/parent observation when
|
||||
# this legacy sync bridge needs a worker thread; otherwise a provider
|
||||
# created inside the worker sees no active correlation context and the
|
||||
# optional langfuse.openai wrapper may emit a standalone OpenAI-generation
|
||||
# trace.
|
||||
from contextvars import copy_context
|
||||
|
||||
context = copy_context()
|
||||
with ThreadPoolExecutor(max_workers=1, thread_name_prefix="guardrail-compat") as executor:
|
||||
return executor.submit(lambda: asyncio.run(_call())).result()
|
||||
return executor.submit(context.run, lambda: asyncio.run(_call())).result()
|
||||
|
||||
def classify(
|
||||
self,
|
||||
|
||||
@@ -321,10 +321,27 @@ class OCICompatibleOpenAIProvider(LLMProvider):
|
||||
getattr(settings, "ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION", None)
|
||||
or os.getenv("ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION", "false")
|
||||
).strip().lower() in {"1", "true", "yes", "on", "y"}
|
||||
if self.telemetry is not None and use_langfuse_wrapper:
|
||||
|
||||
# The framework owns Langfuse correlation. Even compatibility paths may
|
||||
# instantiate a provider without passing ``Telemetry`` explicitly while a
|
||||
# request is already active (for example GuardrailLLMClient running in its
|
||||
# sync bridge). In that situation langfuse.openai would auto-create an
|
||||
# ``OpenAI-generation`` root trace instead of attaching to the business
|
||||
# request. Treat an active framework observability context exactly like
|
||||
# an injected Telemetry instance and keep the standard OpenAI client.
|
||||
active_framework_trace = False
|
||||
try:
|
||||
from agent_framework.observability.context import get_observability_context
|
||||
|
||||
obs_ctx = get_observability_context()
|
||||
active_framework_trace = bool(obs_ctx.trace_id or obs_ctx.request_id)
|
||||
except Exception:
|
||||
active_framework_trace = False
|
||||
|
||||
if use_langfuse_wrapper and (self.telemetry is not None or active_framework_trace):
|
||||
logger.warning(
|
||||
"ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true ignorado porque o provider já recebeu "
|
||||
"Telemetry do framework; instrumentação dupla pode criar observations fora do contrato de mapping."
|
||||
"ENABLE_LANGFUSE_OPENAI_AUTO_INSTRUMENTATION=true ignorado durante execução correlacionada "
|
||||
"do framework; langfuse.openai pode criar OpenAI-generation como trace raiz separado."
|
||||
)
|
||||
use_langfuse_wrapper = False
|
||||
if getattr(settings, "ENABLE_LANGFUSE", False) and use_langfuse_wrapper:
|
||||
|
||||
Reference in New Issue
Block a user