diff --git a/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/__pycache__/agent_runtime.cpython-313.pyc b/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/__pycache__/agent_runtime.cpython-313.pyc index 7be12963..23e52040 100644 Binary files a/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/__pycache__/agent_runtime.cpython-313.pyc and b/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/__pycache__/agent_runtime.cpython-313.pyc differ diff --git a/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/__pycache__/transaction_parameters.cpython-313.pyc b/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/__pycache__/transaction_parameters.cpython-313.pyc index aa307ec2..dce1b61d 100644 Binary files a/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/__pycache__/transaction_parameters.cpython-313.pyc and b/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/__pycache__/transaction_parameters.cpython-313.pyc differ diff --git a/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py b/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py index aeaabeca..62201b01 100644 --- a/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py +++ b/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/agent_runtime.py @@ -3008,48 +3008,38 @@ 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) + state.pop("transaction_parameter_reconciliation", None) # Argumentos estruturados já presentes no contexto são aceitos de # forma genérica (não são parsing textual). Para required fields, @@ -3096,65 +3086,11 @@ 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: - return [pre_validation_result] + # 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( state, tool_name=tool_name, arguments=arguments, policy=policy diff --git a/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/transaction_parameters.py b/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/transaction_parameters.py index 88cfbeb6..4ba3b879 100644 --- a/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/transaction_parameters.py +++ b/agent_framework_oci/libs/agent_framework/src/agent_framework/runtime/transaction_parameters.py @@ -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