bugfix: extractor cases 8 and 16
This commit is contained in:
Binary file not shown.
Binary file not shown.
@@ -3008,47 +3008,37 @@ class AgentRuntimeMixin:
|
||||
policy = self._resolve_tool_execution_policy(tool_name, previous_args)
|
||||
missing_before = self._missing_required_arguments(policy, previous_args)
|
||||
|
||||
# Primeira tentativa: coleta tradicional usando SOMENTE a fala
|
||||
# atual. O histórico não deve participar enquanto a mensagem corrente
|
||||
# conseguir preencher o contrato normalmente.
|
||||
# Uma única fonte de verdade para parâmetros: o extractor recebe
|
||||
# todos os required fields, valores já conhecidos e o contexto
|
||||
# bounded da própria transação. Assim ele pode resolver referências
|
||||
# e correções explícitas sem uma segunda máquina de reconciliação.
|
||||
arguments = dict(previous_args)
|
||||
extracted_current = await self._extract_transaction_parameters_current_only(
|
||||
required_fields = [str(name) for name in (policy.get("requires") or [])]
|
||||
collector_override = getattr(type(self), "_extract_transaction_parameters", None)
|
||||
collector_fields = (
|
||||
list(missing_before)
|
||||
if collector_override is not None and collector_override is not AgentRuntimeMixin._extract_transaction_parameters
|
||||
else required_fields
|
||||
)
|
||||
extracted_current = await self._extract_transaction_parameters(
|
||||
state,
|
||||
tool_name=tool_name,
|
||||
missing_parameters=missing_before,
|
||||
missing_parameters=collector_fields,
|
||||
known_arguments=previous_args,
|
||||
)
|
||||
arguments.update(dict(extracted_current or {}))
|
||||
route_meta_collection = ((state.get("route_decision") or {}).get("metadata") or {}) if isinstance(state.get("route_decision"), dict) else {}
|
||||
router_values = route_meta_collection.get("transaction_parameter_values")
|
||||
router_fields = sorted(
|
||||
str(k) for k, v in (router_values.items() if isinstance(router_values, dict) else [])
|
||||
if v not in _EMPTY_VALUES
|
||||
)
|
||||
state["transaction_parameter_collection"] = {
|
||||
"tool_name": tool_name,
|
||||
"mode": "current_turn",
|
||||
"mode": "single_extractor",
|
||||
"resolved_fields": sorted(str(k) for k in (extracted_current or {}).keys()),
|
||||
"router_resolved_fields": router_fields,
|
||||
}
|
||||
|
||||
# Segunda opção: somente se a coleta tradicional ainda deixar
|
||||
# required fields em aberto, reconcilie temporalmente usando o
|
||||
# schema/descrições declarados em tools.yaml e o contexto bounded
|
||||
# newest->oldest preservado na transação.
|
||||
missing_after_current = self._missing_required_arguments(policy, arguments)
|
||||
if missing_after_current:
|
||||
required_fields = [str(name) for name in (policy.get("requires") or [])]
|
||||
reconciliation = await self._reconcile_transaction_parameters(
|
||||
state,
|
||||
tool_name=tool_name,
|
||||
parameter_names=required_fields,
|
||||
known_arguments=arguments,
|
||||
)
|
||||
for field in reconciliation.get("clear_fields") or []:
|
||||
arguments.pop(str(field), None)
|
||||
arguments.update(dict(reconciliation.get("values") or {}))
|
||||
state["transaction_parameter_reconciliation"] = {
|
||||
"tool_name": tool_name,
|
||||
"trigger": "traditional_collection_unresolved",
|
||||
"decisions": dict(reconciliation.get("decisions") or {}),
|
||||
"provenance": dict(reconciliation.get("provenance") or {}),
|
||||
"clear_fields": list(reconciliation.get("clear_fields") or []),
|
||||
}
|
||||
else:
|
||||
state.pop("transaction_parameter_reconciliation", None)
|
||||
|
||||
# Argumentos estruturados já presentes no contexto são aceitos de
|
||||
@@ -3096,64 +3086,10 @@ class AgentRuntimeMixin:
|
||||
state, tool_name=tool_name, arguments=arguments, policy=policy, emit_events=emit_events
|
||||
)
|
||||
if pre_validation_result is not None:
|
||||
# A pre-validation é a fronteira autoritativa da coleta
|
||||
# tradicional. Se ela rejeitar um candidato de forma recuperável
|
||||
# (NEEDS_PARAMETER), ainda não repromptamos o usuário: fazemos UMA
|
||||
# tentativa de reconciliação temporal com o histórico bounded e
|
||||
# com a semântica declarada em tools.yaml. Isso cobre referências
|
||||
# como "é a de R$ 19,99" sem tornar o reconciliador o caminho
|
||||
# primário de COLLECTING_PARAMETERS.
|
||||
pre_meta = state.get("transaction_pre_validation")
|
||||
pre_meta = pre_meta if isinstance(pre_meta, dict) else {}
|
||||
retry_parameter = str(pre_meta.get("parameter") or "").strip()
|
||||
recoverable = (
|
||||
str(pre_meta.get("status") or "").upper() == "NEEDS_PARAMETER"
|
||||
and bool(retry_parameter)
|
||||
and state.get("transaction_status") == "COLLECTING_PARAMETERS"
|
||||
)
|
||||
if recoverable:
|
||||
recovered_selected = dict(self._active_transaction(state) or state.get("selected_tool_call") or {})
|
||||
recovered_arguments = dict(recovered_selected.get("arguments") or {})
|
||||
required_fields = [str(name) for name in (policy.get("requires") or [])]
|
||||
reconciliation = await self._reconcile_transaction_parameters(
|
||||
state,
|
||||
tool_name=tool_name,
|
||||
parameter_names=required_fields,
|
||||
known_arguments=recovered_arguments,
|
||||
)
|
||||
for field in reconciliation.get("clear_fields") or []:
|
||||
recovered_arguments.pop(str(field), None)
|
||||
recovered_arguments.update(dict(reconciliation.get("values") or {}))
|
||||
state["transaction_parameter_reconciliation"] = {
|
||||
"tool_name": tool_name,
|
||||
"trigger": "prevalidation_needs_parameter",
|
||||
"rejected_parameter": retry_parameter,
|
||||
"decisions": dict(reconciliation.get("decisions") or {}),
|
||||
"provenance": dict(reconciliation.get("provenance") or {}),
|
||||
"clear_fields": list(reconciliation.get("clear_fields") or []),
|
||||
}
|
||||
retry_policy = self._resolve_tool_execution_policy(tool_name, recovered_arguments)
|
||||
retry_missing = self._missing_required_arguments(retry_policy, recovered_arguments)
|
||||
if retry_parameter not in retry_missing:
|
||||
state["selected_tool_call"] = {"tool_name": tool_name, "arguments": recovered_arguments}
|
||||
self._set_active_transaction(
|
||||
state, tool_name=tool_name, arguments=recovered_arguments, status="COLLECTING_PARAMETERS"
|
||||
)
|
||||
state["missing_parameters"] = retry_missing
|
||||
pre_validation_retry = await self._run_transaction_pre_validation(
|
||||
state,
|
||||
tool_name=tool_name,
|
||||
arguments=recovered_arguments,
|
||||
policy=retry_policy,
|
||||
emit_events=emit_events,
|
||||
)
|
||||
if pre_validation_retry is None:
|
||||
arguments = recovered_arguments
|
||||
policy = retry_policy
|
||||
pre_validation_result = None
|
||||
else:
|
||||
return [pre_validation_retry]
|
||||
if pre_validation_result is not None:
|
||||
# A pre-validation é a fronteira autoritativa. Se pedir um
|
||||
# parâmetro novamente, o estado produzido por ela é preservado
|
||||
# e o próximo turno volta ao mesmo extractor. Não existe uma
|
||||
# segunda tentativa escondida de reconciliação no mesmo turno.
|
||||
return [pre_validation_result]
|
||||
|
||||
tool_name, policy, force_confirmation = self._apply_prevalidated_transaction_decision(
|
||||
|
||||
@@ -230,20 +230,88 @@ async def extract_transaction_parameters(
|
||||
tool_description: str | None = None,
|
||||
conversational_context: str | None = None,
|
||||
) -> dict[str, Any]:
|
||||
"""Compatibility facade returning only resolved candidates.
|
||||
"""Extract transaction parameter values with one simple source of truth.
|
||||
|
||||
New runtime code should use :func:`reconcile_transaction_parameters` when it
|
||||
needs preserve/clear/provenance decisions. Keeping this facade avoids
|
||||
breaking routers and existing extensions that only need candidate extraction.
|
||||
The current utterance is authoritative for new values/corrections. A bounded
|
||||
conversation context may only be used to resolve references made by the
|
||||
current utterance (for example, "the 14.99 one"). Known arguments are state
|
||||
defaults: if a field is not returned it is preserved by the caller.
|
||||
|
||||
The extractor does not decide preserve/clear/unresolved, routing, confirmation
|
||||
or execution. Business validity remains the responsibility of pre-validation.
|
||||
"""
|
||||
result = await reconcile_transaction_parameters(
|
||||
llm,
|
||||
text=text,
|
||||
tool_name=tool_name,
|
||||
parameter_names=missing_parameters,
|
||||
known_arguments=known_arguments,
|
||||
parameter_schema=parameter_schema,
|
||||
tool_description=tool_description,
|
||||
conversational_context=conversational_context,
|
||||
names = [str(name) for name in (missing_parameters or []) if str(name).strip()]
|
||||
message = str(text or "").strip()
|
||||
if not names or not message or llm is None:
|
||||
return {}
|
||||
|
||||
schema = dict(parameter_schema or {})
|
||||
known = {
|
||||
str(key): value
|
||||
for key, value in dict(known_arguments or {}).items()
|
||||
if value not in _EMPTY_VALUES
|
||||
}
|
||||
field_spec = {
|
||||
name: {
|
||||
"type": schema.get(name, "string") if not isinstance(schema.get(name), dict) else schema.get(name, {}).get("type", "string"),
|
||||
"description": None if not isinstance(schema.get(name), dict) else schema.get(name, {}).get("description"),
|
||||
}
|
||||
for name in names
|
||||
}
|
||||
output_shape = {name: None for name in names}
|
||||
prompt = (
|
||||
"Você extrai parâmetros de uma transação ativa. Não faça roteamento, confirmação, execução ou validação de negócio.\n\n"
|
||||
"REGRAS:\n"
|
||||
"1. O significado de cada campo vem exclusivamente de parameter_schema e transaction_description, considerando principalmente a descrição semântica quando disponível.\n"
|
||||
"2. A ausência de tipo ou descrição NÃO impede a extração quando o restante do contrato e o texto forem semanticamente suficientes.\n3. A fala atual é a fonte principal. Retorne um campo quando a fala atual o informa, corrige ou referencia de forma suficientemente clara.\n"
|
||||
"4. conversational_context é apenas contexto recente para resolver referências da fala atual, como 'esse', 'a de 14,99', 'o primeiro'. Não trate texto do contexto como uma nova afirmação do cliente; use-o somente para interpretar a referência atual.\n"
|
||||
"5. known_arguments contém valores já conhecidos. Não os repita por obrigação. Se a fala atual não os altera, omita o campo ou retorne null; o runtime os preservará.\n"
|
||||
"6. Se a fala atual corrigir explicitamente um valor conhecido, retorne o novo valor. A fala atual tem precedência temporal para esse campo.\n"
|
||||
"7. Você pode retornar mais de um campo quando a combinação da fala atual com o contexto recente determinar a relação de forma clara.\n"
|
||||
"8. Não valide se o valor existe no backend nem se a combinação é permitida. Retorne o candidato textual; a pre-validation fará a validação autoritativa.\n"
|
||||
"9. Em caso de dúvida razoável sobre a correspondência ou o valor, prefira null. Não invente associações.\n"
|
||||
"10. Responda somente JSON válido, sem markdown e sem chaves extras.\n\n"
|
||||
f"transaction_tool: {tool_name}\n"
|
||||
f"transaction_description: {tool_description or ''}\n"
|
||||
f"parameter_names: {json.dumps(names, ensure_ascii=False)}\n"
|
||||
f"pending_parameters: {json.dumps(names, ensure_ascii=False)}\n"
|
||||
f"parameter_schema: {json.dumps(field_spec, ensure_ascii=False, default=str)}\n"
|
||||
f"known_arguments: {json.dumps(known, ensure_ascii=False, default=str)}\n"
|
||||
f"user_message: {message}\n"
|
||||
f"conversational_context: {str(conversational_context or '').strip()}\n"
|
||||
f"Formato obrigatório: {json.dumps(output_shape, ensure_ascii=False)}"
|
||||
)
|
||||
return dict(result.get("values") or {})
|
||||
|
||||
try:
|
||||
response = await llm.ainvoke(
|
||||
[{"role": "user", "content": prompt}],
|
||||
profile_name="transaction_parameter_extraction",
|
||||
component_name="transaction_parameter_extraction",
|
||||
generation_name="llm.transaction_parameter_extraction",
|
||||
temperature=0.0,
|
||||
)
|
||||
except TypeError:
|
||||
response = await llm.ainvoke([{"role": "user", "content": prompt}])
|
||||
except Exception as exc:
|
||||
logger.warning("transaction.parameter.extract_failed tool=%s fields=%s error=%s", tool_name, names, exc)
|
||||
return {}
|
||||
|
||||
raw = _response_text(response).strip()
|
||||
try:
|
||||
payload = parse_json_object(raw)
|
||||
except (TypeError, ValueError):
|
||||
logger.warning("transaction.parameter.extract_invalid_output tool=%s raw=%r", tool_name, raw[:240])
|
||||
return {}
|
||||
if not isinstance(payload, dict):
|
||||
return {}
|
||||
|
||||
values: dict[str, Any] = {}
|
||||
for name in names:
|
||||
value = payload.get(name)
|
||||
if value in _EMPTY_VALUES:
|
||||
continue
|
||||
declared = field_spec.get(name, {}).get("type", "string")
|
||||
coerced = _coerce(value, declared)
|
||||
if coerced not in _EMPTY_VALUES:
|
||||
values[name] = coerced
|
||||
return values
|
||||
|
||||
Reference in New Issue
Block a user