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)
|
policy = self._resolve_tool_execution_policy(tool_name, previous_args)
|
||||||
missing_before = self._missing_required_arguments(policy, previous_args)
|
missing_before = self._missing_required_arguments(policy, previous_args)
|
||||||
|
|
||||||
# Primeira tentativa: coleta tradicional usando SOMENTE a fala
|
# Uma única fonte de verdade para parâmetros: o extractor recebe
|
||||||
# atual. O histórico não deve participar enquanto a mensagem corrente
|
# todos os required fields, valores já conhecidos e o contexto
|
||||||
# conseguir preencher o contrato normalmente.
|
# 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)
|
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,
|
state,
|
||||||
tool_name=tool_name,
|
tool_name=tool_name,
|
||||||
missing_parameters=missing_before,
|
missing_parameters=collector_fields,
|
||||||
known_arguments=previous_args,
|
known_arguments=previous_args,
|
||||||
)
|
)
|
||||||
arguments.update(dict(extracted_current or {}))
|
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"] = {
|
state["transaction_parameter_collection"] = {
|
||||||
"tool_name": tool_name,
|
"tool_name": tool_name,
|
||||||
"mode": "current_turn",
|
"mode": "single_extractor",
|
||||||
"resolved_fields": sorted(str(k) for k in (extracted_current or {}).keys()),
|
"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)
|
state.pop("transaction_parameter_reconciliation", None)
|
||||||
|
|
||||||
# Argumentos estruturados já presentes no contexto são aceitos de
|
# 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
|
state, tool_name=tool_name, arguments=arguments, policy=policy, emit_events=emit_events
|
||||||
)
|
)
|
||||||
if pre_validation_result is not None:
|
if pre_validation_result is not None:
|
||||||
# A pre-validation é a fronteira autoritativa da coleta
|
# A pre-validation é a fronteira autoritativa. Se pedir um
|
||||||
# tradicional. Se ela rejeitar um candidato de forma recuperável
|
# parâmetro novamente, o estado produzido por ela é preservado
|
||||||
# (NEEDS_PARAMETER), ainda não repromptamos o usuário: fazemos UMA
|
# e o próximo turno volta ao mesmo extractor. Não existe uma
|
||||||
# tentativa de reconciliação temporal com o histórico bounded e
|
# segunda tentativa escondida de reconciliação no mesmo turno.
|
||||||
# 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:
|
|
||||||
return [pre_validation_result]
|
return [pre_validation_result]
|
||||||
|
|
||||||
tool_name, policy, force_confirmation = self._apply_prevalidated_transaction_decision(
|
tool_name, policy, force_confirmation = self._apply_prevalidated_transaction_decision(
|
||||||
|
|||||||
@@ -230,20 +230,88 @@ async def extract_transaction_parameters(
|
|||||||
tool_description: str | None = None,
|
tool_description: str | None = None,
|
||||||
conversational_context: str | None = None,
|
conversational_context: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> 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
|
The current utterance is authoritative for new values/corrections. A bounded
|
||||||
needs preserve/clear/provenance decisions. Keeping this facade avoids
|
conversation context may only be used to resolve references made by the
|
||||||
breaking routers and existing extensions that only need candidate extraction.
|
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(
|
names = [str(name) for name in (missing_parameters or []) if str(name).strip()]
|
||||||
llm,
|
message = str(text or "").strip()
|
||||||
text=text,
|
if not names or not message or llm is None:
|
||||||
tool_name=tool_name,
|
return {}
|
||||||
parameter_names=missing_parameters,
|
|
||||||
known_arguments=known_arguments,
|
schema = dict(parameter_schema or {})
|
||||||
parameter_schema=parameter_schema,
|
known = {
|
||||||
tool_description=tool_description,
|
str(key): value
|
||||||
conversational_context=conversational_context,
|
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