ummaya 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -2
- package/bin/ummaya +10 -1
- package/npm-shrinkwrap.json +253 -2
- package/package.json +5 -1
- package/prompts/manifest.yaml +1 -1
- package/prompts/system_v1.md +1 -0
- package/pyproject.toml +26 -2
- package/specs/2803-document-production-hardening/contracts/document-tools.schema.json +1043 -0
- package/src/ummaya/_canonical/__init__.py +2 -0
- package/src/ummaya/engine/engine.py +29 -132
- package/src/ummaya/evidence/__init__.py +21 -2
- package/src/ummaya/evidence/dataset_contract.py +193 -0
- package/src/ummaya/evidence/document_authoring_cases.py +33 -0
- package/src/ummaya/evidence/document_harness.py +313 -0
- package/src/ummaya/evidence/document_viewer_ux.py +391 -0
- package/src/ummaya/evidence/gates.py +70 -0
- package/src/ummaya/evidence/json_types.py +20 -0
- package/src/ummaya/evidence/models.py +88 -1
- package/src/ummaya/evidence/output_payload.py +89 -0
- package/src/ummaya/evidence/payload_documents.py +233 -0
- package/src/ummaya/evidence/route_contracts.py +224 -0
- package/src/ummaya/evidence/route_helpers.py +150 -0
- package/src/ummaya/evidence/runner.py +81 -212
- package/src/ummaya/evidence/source_provenance.py +246 -0
- package/src/ummaya/evidence/source_provenance_redaction.py +176 -0
- package/src/ummaya/evidence/tool_layer.py +39 -0
- package/src/ummaya/evidence/tool_layer_models.py +151 -0
- package/src/ummaya/ipc/adapter_manifest_emitter.py +26 -10
- package/src/ummaya/ipc/document_intent_normalization.py +185 -0
- package/src/ummaya/ipc/frame_schema.py +5 -5
- package/src/ummaya/ipc/route_diagnostics.py +73 -0
- package/src/ummaya/ipc/stdio.py +1109 -477
- package/src/ummaya/llm/client.py +102 -3
- package/src/ummaya/llm/config.py +8 -3
- package/src/ummaya/primitives/__init__.py +6 -2
- package/src/ummaya/primitives/delegation.py +1 -1
- package/src/ummaya/primitives/document.py +28 -0
- package/src/ummaya/settings.py +0 -3
- package/src/ummaya/tools/discovery_bridge.py +17 -1
- package/src/ummaya/tools/documents/__init__.py +297 -0
- package/src/ummaya/tools/documents/adapter_registry.py +487 -0
- package/src/ummaya/tools/documents/archive_container_probe.py +167 -0
- package/src/ummaya/tools/documents/artifact_store.py +454 -0
- package/src/ummaya/tools/documents/authoring.py +283 -0
- package/src/ummaya/tools/documents/baselines.py +114 -0
- package/src/ummaya/tools/documents/capability.py +331 -0
- package/src/ummaya/tools/documents/contracts.py +112 -0
- package/src/ummaya/tools/documents/conversion.py +521 -0
- package/src/ummaya/tools/documents/diff.py +275 -0
- package/src/ummaya/tools/documents/engines.py +163 -0
- package/src/ummaya/tools/documents/evaluation.py +291 -0
- package/src/ummaya/tools/documents/explicit_values.py +108 -0
- package/src/ummaya/tools/documents/fixtures.py +174 -0
- package/src/ummaya/tools/documents/format_completion_audit.py +471 -0
- package/src/ummaya/tools/documents/formats/__init__.py +2 -0
- package/src/ummaya/tools/documents/formats/archive.py +528 -0
- package/src/ummaya/tools/documents/formats/base.py +41 -0
- package/src/ummaya/tools/documents/formats/code_file.py +211 -0
- package/src/ummaya/tools/documents/formats/data_file.py +272 -0
- package/src/ummaya/tools/documents/formats/hwp.py +284 -0
- package/src/ummaya/tools/documents/formats/hwpx.py +1837 -0
- package/src/ummaya/tools/documents/formats/odf.py +435 -0
- package/src/ummaya/tools/documents/formats/ooxml.py +1030 -0
- package/src/ummaya/tools/documents/formats/passive.py +766 -0
- package/src/ummaya/tools/documents/formats/pdf.py +702 -0
- package/src/ummaya/tools/documents/formats/text_web.py +268 -0
- package/src/ummaya/tools/documents/hwp_conversion_probe.py +178 -0
- package/src/ummaya/tools/documents/hwp_direct_candidate.py +141 -0
- package/src/ummaya/tools/documents/inspection.py +289 -0
- package/src/ummaya/tools/documents/intake.py +1079 -0
- package/src/ummaya/tools/documents/legacy_office_promotion_probe.py +366 -0
- package/src/ummaya/tools/documents/models.py +1598 -0
- package/src/ummaya/tools/documents/odf_promotion_probe.py +167 -0
- package/src/ummaya/tools/documents/orchestrator.py +96 -0
- package/src/ummaya/tools/documents/passive_capability_probe.py +251 -0
- package/src/ummaya/tools/documents/patch.py +170 -0
- package/src/ummaya/tools/documents/pdfa_conformance.py +284 -0
- package/src/ummaya/tools/documents/pdfa_promotion_probe.py +198 -0
- package/src/ummaya/tools/documents/permissions.py +110 -0
- package/src/ummaya/tools/documents/planner.py +616 -0
- package/src/ummaya/tools/documents/registry.py +2733 -0
- package/src/ummaya/tools/documents/render.py +978 -0
- package/src/ummaya/tools/documents/render_comparison.py +113 -0
- package/src/ummaya/tools/documents/render_comparison_models.py +74 -0
- package/src/ummaya/tools/documents/render_comparison_regions.py +73 -0
- package/src/ummaya/tools/documents/render_comparison_style.py +161 -0
- package/src/ummaya/tools/documents/reread.py +157 -0
- package/src/ummaya/tools/documents/runtime_authoring.py +244 -0
- package/src/ummaya/tools/documents/runtime_authoring_bundle.py +76 -0
- package/src/ummaya/tools/documents/scorecard.py +184 -0
- package/src/ummaya/tools/documents/socratic_planner.py +193 -0
- package/src/ummaya/tools/documents/style.py +48 -0
- package/src/ummaya/tools/documents/tool_defs.py +523 -0
- package/src/ummaya/tools/documents/validate.py +347 -0
- package/src/ummaya/tools/executor.py +29 -0
- package/src/ummaya/tools/live_proxy.py +0 -3
- package/src/ummaya/tools/models.py +5 -1
- package/src/ummaya/tools/register_all.py +8 -0
- package/src/ummaya/tools/registry.py +10 -1
- package/src/ummaya/tools/routing/__init__.py +59 -0
- package/src/ummaya/tools/routing/builder.py +105 -0
- package/src/ummaya/tools/routing/cards.py +29 -0
- package/src/ummaya/tools/routing/decision_service.py +534 -0
- package/src/ummaya/tools/routing/decision_types.py +74 -0
- package/src/ummaya/tools/routing/feasibility.py +122 -0
- package/src/ummaya/tools/routing/intent.py +17 -0
- package/src/ummaya/tools/routing/intent_extractor.py +207 -0
- package/src/ummaya/tools/routing/intent_patterns.py +160 -0
- package/src/ummaya/tools/routing/intent_public_data.py +150 -0
- package/src/ummaya/tools/routing/intent_types.py +48 -0
- package/src/ummaya/tools/routing/lint.py +78 -0
- package/src/ummaya/tools/routing/metadata.py +174 -0
- package/src/ummaya/tools/routing/projection.py +340 -0
- package/src/ummaya/tools/routing/retrieval_policy.py +629 -0
- package/src/ummaya/tools/routing/schema.py +81 -0
- package/src/ummaya/tools/routing/types.py +96 -0
- package/src/ummaya/tools/routing_index.py +2 -2
- package/src/ummaya/tools/search.py +34 -746
- package/tests/fixtures/documents/public_forms/baselines.yaml +113 -0
- package/tui/package.json +1 -1
- package/tui/src/.cc-byte-identical-whitelist.yaml +266 -0
- package/tui/src/QueryEngine.ts +12 -8
- package/tui/src/bridge/inboundAttachments.ts +3 -3
- package/tui/src/cli/handlers/auth.ts +3 -12
- package/tui/src/cli/print.ts +7 -7
- package/tui/src/commands/insights.ts +1 -1
- package/tui/src/commands/install-github-app/types.ts +8 -30
- package/tui/src/commands/plugin/types.ts +6 -28
- package/tui/src/commands/plugin/unifiedTypes.ts +4 -26
- package/tui/src/commands/rename/generateSessionName.ts +1 -1
- package/tui/src/components/Feedback.tsx +1 -1
- package/tui/src/components/LogoV2/EmergencyTip.tsx +11 -2
- package/tui/src/components/LogoV2/WelcomeV2.tsx +1 -3
- package/tui/src/components/ScrollKeybindingHandler.tsx +6 -6
- package/tui/src/components/Spinner/types.ts +6 -28
- package/tui/src/components/agents/generateAgent.ts +1 -1
- package/tui/src/components/agents/new-agent-creation/types.ts +4 -26
- package/tui/src/components/config/EnvSecretIsolatedEditor.tsx +1 -1
- package/tui/src/components/mcp/types.ts +16 -38
- package/tui/src/components/messages/AssistantToolUseMessage.tsx +3 -2
- package/tui/src/components/messages/UserCrossSessionMessage.ts +16 -4
- package/tui/src/components/messages/UserForkBoilerplateMessage.ts +16 -4
- package/tui/src/components/messages/UserGitHubWebhookMessage.ts +16 -4
- package/tui/src/components/messages/UserToolResultMessage/utils.tsx +3 -2
- package/tui/src/components/permissions/MonitorPermissionRequest/MonitorPermissionRequest.ts +9 -4
- package/tui/src/components/permissions/ReviewArtifactPermissionRequest/ReviewArtifactPermissionRequest.ts +9 -4
- package/tui/src/components/primitive/DocumentSocraticReviewBlock.tsx +129 -0
- package/tui/src/components/primitive/DocumentToolResultCard.tsx +224 -0
- package/tui/src/components/primitive/documentSocraticReview.ts +215 -0
- package/tui/src/components/primitive/index.tsx +43 -1
- package/tui/src/components/primitive/types.ts +137 -0
- package/tui/src/components/ui/option.ts +4 -26
- package/tui/src/constants/common.ts +0 -2
- package/tui/src/constants/prompts.ts +4 -3
- package/tui/src/constants/querySource.ts +4 -26
- package/tui/src/entrypoints/sdk/controlTypes.ts +26 -48
- package/tui/src/entrypoints/sdk/coreTypes.generated.ts +3 -25
- package/tui/src/entrypoints/sdk/runtimeTypes.ts +38 -60
- package/tui/src/entrypoints/sdk/sdkUtilityTypes.ts +4 -26
- package/tui/src/entrypoints/sdk/settingsTypes.generated.ts +3 -25
- package/tui/src/entrypoints/sdk/toolTypes.ts +3 -25
- package/tui/src/hooks/toolPermission/handlers/interactiveHandler.ts +10 -0
- package/tui/src/hooks/useApiKeyVerification.ts +1 -1
- package/tui/src/hooks/useVirtualScroll.ts +1 -1
- package/tui/src/ink/ink.tsx +33 -14
- package/tui/src/ink/reconciler.ts +2 -3
- package/tui/src/ink/render-to-screen.ts +30 -10
- package/tui/src/ipc/bridge.ts +62 -15
- package/tui/src/ipc/bridgeSingleton.ts +5 -1
- package/tui/src/ipc/codec.ts +3 -3
- package/tui/src/ipc/frames.generated.ts +12 -12
- package/tui/src/ipc/llmClient.ts +151 -27
- package/tui/src/ipc/schema/frame.schema.json +1 -1
- package/tui/src/keybindings/defaultBindings.ts +4 -0
- package/tui/src/main.tsx +29 -11
- package/tui/src/native-ts/file-index/index.ts +33 -3
- package/tui/src/observability/surface.ts +2 -2
- package/tui/src/probes/toolRegistryProbe.tsx +3 -1
- package/tui/src/projectOnboardingState.ts +7 -6
- package/tui/src/query/chatMessageTypes.ts +18 -0
- package/tui/src/query/chatMessagesBuilder.ts +1 -1
- package/tui/src/query/deps.ts +1 -1
- package/tui/src/query/messageGuards.ts +106 -0
- package/tui/src/query/publicDataTerminalRepair.ts +384 -0
- package/tui/src/query/run.ts +1075 -0
- package/tui/src/query/supportBoundary.ts +168 -0
- package/tui/src/query/toolResultErrors.ts +103 -0
- package/tui/src/query/toolRunner.ts +687 -0
- package/tui/src/query/unavailableToolRepair.ts +118 -0
- package/tui/src/query.ts +9 -2186
- package/tui/src/screens/REPL.tsx +40 -29
- package/tui/src/services/api/adapterManifest.ts +4 -0
- package/tui/src/services/api/backendChat/events.ts +117 -0
- package/tui/src/services/api/backendChat/finalMessage.ts +40 -0
- package/tui/src/services/api/backendChat/frame.ts +9 -0
- package/tui/src/services/api/backendChat/streaming.ts +430 -0
- package/tui/src/services/api/backendChat/types.ts +62 -0
- package/tui/src/services/api/backendChat.ts +1 -0
- package/tui/src/services/api/client.ts +65 -2
- package/tui/src/services/api/errorUtils.ts +5 -5
- package/tui/src/services/api/errors.ts +1 -1
- package/tui/src/services/api/logging.ts +1 -1
- package/tui/src/services/api/ummaya/evidence.ts +194 -0
- package/tui/src/services/api/ummaya/messages.ts +255 -0
- package/tui/src/services/api/ummaya/nonStreaming.ts +66 -0
- package/tui/src/services/api/ummaya/provider.ts +200 -0
- package/tui/src/services/api/ummaya/reasoning.ts +24 -0
- package/tui/src/services/api/ummaya/request.ts +200 -0
- package/tui/src/services/api/ummaya/selectionContext.ts +240 -0
- package/tui/src/services/api/ummaya/streaming.ts +365 -0
- package/tui/src/services/api/ummaya/streamingPayload.ts +129 -0
- package/tui/src/services/api/ummaya/streamingReader.ts +40 -0
- package/tui/src/services/api/ummaya/toolSelection.ts +217 -0
- package/tui/src/services/api/ummaya/types.ts +110 -0
- package/tui/src/services/api/ummaya/usage.ts +30 -0
- package/tui/src/services/api/ummaya.ts +26 -418
- package/tui/src/services/api/withRetry.ts +1 -1
- package/tui/src/services/awaySummary.ts +2 -2
- package/tui/src/services/claudeAiLimits.ts +1 -1
- package/tui/src/services/compact/autoCompact.ts +1 -1
- package/tui/src/services/compact/compact.ts +1 -1
- package/tui/src/services/lsp/types.ts +8 -30
- package/tui/src/services/tips/types.ts +6 -28
- package/tui/src/services/tokenEstimation.ts +1 -1
- package/tui/src/services/toolRegistry/bootGuard.ts +5 -5
- package/tui/src/services/toolUseSummary/toolUseSummaryGenerator.ts +1 -1
- package/tui/src/services/tools/toolExecution.ts +94 -1
- package/tui/src/store/pendingPermissionSlot.ts +1 -1
- package/tui/src/store/session-store.ts +10 -36
- package/tui/src/stubs/any-stub.ts +15 -10
- package/tui/src/stubs/color-diff-napi.ts +37 -23
- package/tui/src/stubs/globals.d.ts +3 -3
- package/tui/src/stubs/macro-preload.ts +23 -12
- package/tui/src/tools/AdapterTool/AdapterTool.ts +1207 -714
- package/tui/src/tools/AdapterTool/routeDiagnostics.ts +75 -0
- package/tui/src/tools/AgentTool/AgentTool.tsx +84 -1371
- package/tui/src/tools/AgentTool/agentToolHandoff.ts +114 -0
- package/tui/src/tools/AgentTool/agentToolPartialResult.ts +16 -0
- package/tui/src/tools/AgentTool/agentToolProgress.ts +32 -0
- package/tui/src/tools/AgentTool/agentToolResolver.ts +161 -0
- package/tui/src/tools/AgentTool/agentToolResult.ts +163 -0
- package/tui/src/tools/AgentTool/agentToolUtils.ts +14 -686
- package/tui/src/tools/AgentTool/asyncAgentLifecycle.ts +208 -0
- package/tui/src/tools/AgentTool/asyncLifecycle.ts +153 -0
- package/tui/src/tools/AgentTool/backgroundedCompletion.ts +126 -0
- package/tui/src/tools/AgentTool/backgroundedLifecycle.ts +174 -0
- package/tui/src/tools/AgentTool/foregroundBackground.ts +83 -0
- package/tui/src/tools/AgentTool/foregroundDrain.tsx +133 -0
- package/tui/src/tools/AgentTool/foregroundFinalize.ts +98 -0
- package/tui/src/tools/AgentTool/foregroundLifecycle.tsx +237 -0
- package/tui/src/tools/AgentTool/foregroundProgress.tsx +169 -0
- package/tui/src/tools/AgentTool/foregroundTask.ts +89 -0
- package/tui/src/tools/AgentTool/forkSubagent.ts +1 -12
- package/tui/src/tools/AgentTool/forkSubagentGate.ts +34 -0
- package/tui/src/tools/AgentTool/launchRouting.ts +203 -0
- package/tui/src/tools/AgentTool/lifecycle.ts +244 -0
- package/tui/src/tools/AgentTool/mcpRouting.ts +73 -0
- package/tui/src/tools/AgentTool/orchestrationSupport.ts +70 -0
- package/tui/src/tools/AgentTool/permissions.ts +39 -0
- package/tui/src/tools/AgentTool/promptSetup.ts +181 -0
- package/tui/src/tools/AgentTool/remoteRouting.ts +62 -0
- package/tui/src/tools/AgentTool/resultMapping.ts +116 -0
- package/tui/src/tools/AgentTool/resumeAgent.ts +39 -107
- package/tui/src/tools/AgentTool/resumeAgentHelpers.ts +140 -0
- package/tui/src/tools/AgentTool/runAgent.ts +1 -1
- package/tui/src/tools/AgentTool/runtimeConfig.ts +57 -0
- package/tui/src/tools/AgentTool/schemas.ts +196 -0
- package/tui/src/tools/AgentTool/sourceVerificationPropagation.ts +263 -0
- package/tui/src/tools/AgentTool/worktreeLifecycle.ts +105 -0
- package/tui/src/tools/AskUserQuestionTool/AskUserQuestionTool.tsx +174 -202
- package/tui/src/tools/BashTool/BashTool.tsx +71 -1072
- package/tui/src/tools/BashTool/bashCommandHelpers.ts +12 -12
- package/tui/src/tools/BashTool/bashPermissions/astPreflight.ts +173 -0
- package/tui/src/tools/BashTool/bashPermissions/classifierChecks.ts +199 -0
- package/tui/src/tools/BashTool/bashPermissions/compoundGuards.ts +53 -0
- package/tui/src/tools/BashTool/bashPermissions/constants.ts +99 -0
- package/tui/src/tools/BashTool/bashPermissions/index.ts +38 -0
- package/tui/src/tools/BashTool/bashPermissions/legacyMisparsing.ts +62 -0
- package/tui/src/tools/BashTool/bashPermissions/main.ts +135 -0
- package/tui/src/tools/BashTool/bashPermissions/normalizedCommands.ts +33 -0
- package/tui/src/tools/BashTool/bashPermissions/operatorFlow.ts +98 -0
- package/tui/src/tools/BashTool/bashPermissions/permissionChecks.ts +200 -0
- package/tui/src/tools/BashTool/bashPermissions/prefixSuggestions.ts +88 -0
- package/tui/src/tools/BashTool/bashPermissions/promptClassifierRules.ts +125 -0
- package/tui/src/tools/BashTool/bashPermissions/ruleDelegates.ts +19 -0
- package/tui/src/tools/BashTool/bashPermissions/ruleMatching.ts +145 -0
- package/tui/src/tools/BashTool/bashPermissions/sandboxAutoAllow.ts +75 -0
- package/tui/src/tools/BashTool/bashPermissions/subcommandFlow.ts +205 -0
- package/tui/src/tools/BashTool/bashPermissions/subcommandGuards.ts +73 -0
- package/tui/src/tools/BashTool/bashPermissions/subcommandResultHelpers.ts +116 -0
- package/tui/src/tools/BashTool/bashPermissions/types.ts +26 -0
- package/tui/src/tools/BashTool/bashPermissions/wrapperStripping.ts +139 -0
- package/tui/src/tools/BashTool/bashPermissions.ts +26 -2621
- package/tui/src/tools/BashTool/call.ts +202 -0
- package/tui/src/tools/BashTool/callLoader.ts +35 -0
- package/tui/src/tools/BashTool/commandClassification.ts +151 -0
- package/tui/src/tools/BashTool/commandClassificationLoader.ts +40 -0
- package/tui/src/tools/BashTool/cwdReset.ts +33 -0
- package/tui/src/tools/BashTool/lineTruncation.ts +11 -0
- package/tui/src/tools/BashTool/modeValidation.ts +13 -1
- package/tui/src/tools/BashTool/outputPersistence.ts +42 -0
- package/tui/src/tools/BashTool/permissionClassification.ts +66 -0
- package/tui/src/tools/BashTool/permissionLoader.ts +44 -0
- package/tui/src/tools/BashTool/resultLoader.ts +29 -0
- package/tui/src/tools/BashTool/resultMapping.ts +83 -0
- package/tui/src/tools/BashTool/sandboxPolicy.ts +79 -0
- package/tui/src/tools/BashTool/schemas.ts +65 -0
- package/tui/src/tools/BashTool/sedEditExecution.ts +59 -0
- package/tui/src/tools/BashTool/shellExecution.tsx +245 -0
- package/tui/src/tools/BashTool/shellOutputUtils.ts +85 -0
- package/tui/src/tools/BashTool/shellPermissionGauntlet.ts +97 -0
- package/tui/src/tools/BashTool/uiLoader.ts +37 -0
- package/tui/src/tools/BriefTool/upload.ts +1 -1
- package/tui/src/tools/CalculatorTool/parser.ts +2 -2
- package/tui/src/tools/DocumentPrimitive/DocumentPrimitive.ts +262 -0
- package/tui/src/tools/DocumentPrimitive/dispatchNormalization.ts +270 -0
- package/tui/src/tools/DocumentPrimitive/documentDestinationPath.ts +18 -0
- package/tui/src/tools/DocumentPrimitive/documentMutationGuard.ts +22 -0
- package/tui/src/tools/DocumentPrimitive/documentPatchNormalization.ts +248 -0
- package/tui/src/tools/DocumentPrimitive/documentSourceVerification.ts +245 -0
- package/tui/src/tools/DocumentPrimitive/documentSourceVerificationFields.ts +103 -0
- package/tui/src/tools/DocumentPrimitive/modelVisibleOutput.ts +40 -0
- package/tui/src/tools/DocumentPrimitive/prompt.ts +35 -0
- package/tui/src/tools/FileEditTool/FileEditTool.ts +9 -507
- package/tui/src/tools/FileEditTool/call.ts +228 -0
- package/tui/src/tools/FileEditTool/validateInput.ts +196 -0
- package/tui/src/tools/FileReadTool/imageProcessor.ts +13 -0
- package/tui/src/tools/FileWriteTool/FileWriteTool.ts +7 -300
- package/tui/src/tools/FileWriteTool/call.ts +223 -0
- package/tui/src/tools/FileWriteTool/validateInput.ts +80 -0
- package/tui/src/tools/ListMcpResourcesTool/ListMcpResourcesTool.ts +19 -3
- package/tui/src/tools/LookupPrimitive/LookupPrimitive.ts +25 -32
- package/tui/src/tools/LookupPrimitive/prompt.ts +0 -2
- package/tui/src/tools/MCPTool/trustPolicy.ts +118 -0
- package/tui/src/tools/McpAuthTool/McpAuthTool.ts +21 -3
- package/tui/src/tools/NotebookEditTool/NotebookEditTool.ts +7 -326
- package/tui/src/tools/NotebookEditTool/call.ts +254 -0
- package/tui/src/tools/NotebookEditTool/notebookModel.ts +51 -0
- package/tui/src/tools/NotebookEditTool/validateInput.ts +142 -0
- package/tui/src/tools/PowerShellTool/PowerShellTool.tsx +46 -937
- package/tui/src/tools/PowerShellTool/acceptEditsCommandValidation.ts +162 -0
- package/tui/src/tools/PowerShellTool/call.ts +179 -0
- package/tui/src/tools/PowerShellTool/callLoader.ts +37 -0
- package/tui/src/tools/PowerShellTool/commandClassification.ts +86 -0
- package/tui/src/tools/PowerShellTool/modeValidation.ts +25 -332
- package/tui/src/tools/PowerShellTool/outputPersistence.ts +42 -0
- package/tui/src/tools/PowerShellTool/permissionClassification.ts +28 -0
- package/tui/src/tools/PowerShellTool/resultLoader.ts +31 -0
- package/tui/src/tools/PowerShellTool/resultMapping.ts +75 -0
- package/tui/src/tools/PowerShellTool/schemas.ts +40 -0
- package/tui/src/tools/PowerShellTool/shellExecution.tsx +258 -0
- package/tui/src/tools/PowerShellTool/symlinkModeValidation.ts +44 -0
- package/tui/src/tools/PowerShellTool/uiLoader.ts +37 -0
- package/tui/src/tools/PowerShellTool/validation.ts +39 -0
- package/tui/src/tools/ReadMcpResourceTool/ReadMcpResourceTool.ts +19 -3
- package/tui/src/tools/ResolveLocationPrimitive/ResolveLocationPrimitive.ts +1 -11
- package/tui/src/tools/ResolveLocationPrimitive/prompt.ts +2 -6
- package/tui/src/tools/SkillTool/SkillTool.ts +2 -2
- package/tui/src/tools/SubmitPrimitive/SubmitPrimitive.ts +27 -10
- package/tui/src/tools/TaskCreateTool/TaskCreateTool.ts +16 -2
- package/tui/src/tools/TaskGetTool/TaskGetTool.ts +23 -3
- package/tui/src/tools/TaskListTool/TaskListTool.ts +22 -4
- package/tui/src/tools/TaskOutputTool/TaskOutputTool.tsx +46 -547
- package/tui/src/tools/TaskOutputTool/lookup.ts +216 -0
- package/tui/src/tools/TaskOutputTool/render.tsx +257 -0
- package/tui/src/tools/TaskOutputTool/schemas.ts +55 -0
- package/tui/src/tools/TaskOutputTool/serialization.ts +36 -0
- package/tui/src/tools/TaskStopTool/TaskStopTool.ts +10 -0
- package/tui/src/tools/TaskUpdateTool/TaskUpdateTool.ts +14 -364
- package/tui/src/tools/TaskUpdateTool/completion.ts +62 -0
- package/tui/src/tools/TaskUpdateTool/schemas.ts +62 -0
- package/tui/src/tools/TaskUpdateTool/serialization.ts +46 -0
- package/tui/src/tools/TaskUpdateTool/statusUpdate.ts +247 -0
- package/tui/src/tools/TodoWriteTool/TodoWriteTool.ts +21 -2
- package/tui/src/tools/ToolSearchTool/ToolSearchTool.ts +21 -302
- package/tui/src/tools/ToolSearchTool/ccSupportTools.ts +223 -0
- package/tui/src/tools/ToolSearchTool/descriptionCache.ts +50 -0
- package/tui/src/tools/ToolSearchTool/keywordSearch.ts +216 -0
- package/tui/src/tools/ToolSearchTool/prompt.ts +10 -4
- package/tui/src/tools/ToolSearchTool/resultMapping.ts +30 -0
- package/tui/src/tools/ToolSearchTool/schemas.ts +30 -0
- package/tui/src/tools/ToolSearchTool/searchPool.ts +47 -0
- package/tui/src/tools/ToolSearchTool/supportIntentHints.ts +140 -0
- package/tui/src/tools/TranslateTool/TranslateTool.ts +1 -1
- package/tui/src/tools/VerifyPrimitive/VerifyPrimitive.ts +2 -1
- package/tui/src/tools/WebFetchTool/WebFetchTool.ts +43 -138
- package/tui/src/tools/WebFetchTool/call.ts +227 -0
- package/tui/src/tools/WebFetchTool/resolvedAddressSafety.ts +78 -0
- package/tui/src/tools/WebFetchTool/sourceVerification.ts +204 -0
- package/tui/src/tools/WebFetchTool/types.ts +23 -0
- package/tui/src/tools/WebFetchTool/urlSafety.ts +181 -0
- package/tui/src/tools/WebFetchTool/utils.ts +1 -1
- package/tui/src/tools/WebSearchTool/UI.tsx +0 -1
- package/tui/src/tools/WebSearchTool/WebSearchTool.ts +9 -313
- package/tui/src/tools/WebSearchTool/call.ts +33 -0
- package/tui/src/tools/WebSearchTool/responseMapping.ts +190 -0
- package/tui/src/tools/WebSearchTool/resultBlock.ts +47 -0
- package/tui/src/tools/WebSearchTool/schemas.ts +47 -0
- package/tui/src/tools/WebSearchTool/toolSchema.ts +12 -0
- package/tui/src/tools/WorkspaceToolAdapter/WorkspaceToolAdapter.ts +79 -0
- package/tui/src/tools/WorkspaceToolAdapter/allowedRootPolicy.ts +85 -0
- package/tui/src/tools/WorkspaceToolAdapter/documentFormatGuards.ts +73 -0
- package/tui/src/tools/WorkspaceToolAdapter/inputNormalization.ts +105 -0
- package/tui/src/tools/WorkspaceToolAdapter/mcpExposurePolicy.ts +64 -0
- package/tui/src/tools/WorkspaceToolAdapter/toolDefFactory.ts +215 -0
- package/tui/src/tools/WorkspaceToolAdapter/toolNames.ts +6 -0
- package/tui/src/tools/WorkspaceToolAdapter/workspacePolicy.ts +15 -0
- package/tui/src/tools/_shared/dispatchPrimitive.ts +6 -6
- package/tui/src/tools/_shared/documentChangeToPatch.ts +125 -0
- package/tui/src/tools/_shared/documentDispatchArguments.ts +87 -0
- package/tui/src/tools/_shared/documentPrimitiveTimeout.ts +13 -0
- package/tui/src/tools/_shared/documentToolResultRender.ts +98 -0
- package/tui/src/tools/_shared/pendingCallRegistry.ts +1 -6
- package/tui/src/tools/_shared/rootPrimitiveInput.ts +1 -0
- package/tui/src/tools/_shared/toolChoiceRepair/documentCompletionPatterns.ts +58 -0
- package/tui/src/tools/_shared/toolChoiceRepair/documentCompletionPrompt.ts +271 -0
- package/tui/src/tools/_shared/toolChoiceRepair/documentRepair.ts +452 -0
- package/tui/src/tools/_shared/toolChoiceRepair/messageAccess.ts +80 -0
- package/tui/src/tools/_shared/toolChoiceRepair/publicDataRepair.ts +92 -0
- package/tui/src/tools/_shared/toolChoiceRepair/supportRepair.ts +135 -0
- package/tui/src/tools/_shared/toolChoiceRepair.ts +55 -860
- package/tui/src/tools/shared/mockDisclaimer.ts +1 -1
- package/tui/src/tools.ts +39 -190
- package/tui/src/types/fileSuggestion.ts +4 -26
- package/tui/src/types/generated/events_mono/claude_code/v1/claude_code_internal_event.ts +186 -148
- package/tui/src/types/generated/events_mono/common/v1/auth.ts +25 -11
- package/tui/src/types/generated/events_mono/growthbook/v1/growthbook_experiment_event.ts +47 -30
- package/tui/src/types/generated/google/protobuf/timestamp.ts +21 -7
- package/tui/src/types/message.ts +80 -102
- package/tui/src/types/messageQueueTypes.ts +6 -28
- package/tui/src/types/notebook.ts +16 -38
- package/tui/src/types/statusLine.ts +4 -26
- package/tui/src/types/tools.ts +24 -46
- package/tui/src/types/utils.ts +6 -28
- package/tui/src/upstreamproxy/relay.ts +7 -3
- package/tui/src/upstreamproxy/upstreamproxy.ts +1 -1
- package/tui/src/utils/assistantMessageFactories.ts +9 -3
- package/tui/src/utils/auth.ts +129 -139
- package/tui/src/utils/bash/ast.ts +23 -23
- package/tui/src/utils/bash/bashParser.ts +5 -5
- package/tui/src/utils/billing.ts +1 -1
- package/tui/src/utils/collapseReadSearch.ts +3 -3
- package/tui/src/utils/cronTasks.ts +1 -1
- package/tui/src/utils/execFileNoThrow.ts +1 -1
- package/tui/src/utils/filePersistence/types.ts +16 -38
- package/tui/src/utils/forkedAgent.ts +1 -1
- package/tui/src/utils/gracefulShutdown.ts +4 -4
- package/tui/src/utils/heapDumpService.ts +12 -8
- package/tui/src/utils/hooks/apiQueryHookHelper.ts +1 -1
- package/tui/src/utils/hooks/execPromptHook.ts +1 -1
- package/tui/src/utils/hooks/skillImprovement.ts +1 -1
- package/tui/src/utils/mcp/dateTimeParser.ts +1 -1
- package/tui/src/utils/messages.ts +18 -0
- package/tui/src/utils/migrateSessions.ts +3 -3
- package/tui/src/utils/model/model.ts +6 -6
- package/tui/src/utils/permissions/yoloClassifier.ts +1 -1
- package/tui/src/utils/plugins/headlessPluginInstall.ts +1 -1
- package/tui/src/utils/plugins/mcpPluginIntegration.ts +1 -1
- package/tui/src/utils/plugins/mcpbHandler.ts +1 -1
- package/tui/src/utils/plugins/pluginLoader.ts +8 -8
- package/tui/src/utils/protectedNamespace.ts +5 -3
- package/tui/src/utils/rawJsonToolCall.ts +242 -0
- package/tui/src/utils/ripgrep.ts +16 -7
- package/tui/src/utils/sessionTitle.ts +1 -1
- package/tui/src/utils/settings/permissionValidation.ts +14 -2
- package/tui/src/utils/shell/prefix.ts +1 -1
- package/tui/src/utils/sideQuery.ts +1 -1
- package/tui/src/utils/systemThemeWatcher.ts +13 -3
- package/tui/src/utils/teleport.tsx +1 -1
- package/uv.lock +400 -14
- package/tui/src/services/api/claude.ts +0 -3540
- package/tui/src/tools/_shared/directPublicDataGuard.ts +0 -362
- package/tui/src/tools/_shared/kmaAnalysisGuard.ts +0 -197
- package/tui/src/tools/_shared/kmaAviationGuard.ts +0 -70
- package/tui/src/tools/_shared/nmcAedGuard.ts +0 -234
- package/tui/src/tools/_shared/protectedCheckGuard.ts +0 -207
- package/tui/src/tools/_shared/textToolCallGuard.ts +0 -91
package/src/ummaya/ipc/stdio.py
CHANGED
|
@@ -33,7 +33,7 @@ import signal
|
|
|
33
33
|
import sys
|
|
34
34
|
import time
|
|
35
35
|
import uuid
|
|
36
|
-
from collections.abc import Callable, Collection
|
|
36
|
+
from collections.abc import Callable, Collection, Iterable
|
|
37
37
|
from datetime import UTC, datetime, timedelta
|
|
38
38
|
from types import FrameType
|
|
39
39
|
from typing import TYPE_CHECKING, Any, Final, Literal, cast
|
|
@@ -42,6 +42,10 @@ from opentelemetry import trace
|
|
|
42
42
|
from opentelemetry.trace import Status, StatusCode
|
|
43
43
|
from pydantic import TypeAdapter, ValidationError
|
|
44
44
|
|
|
45
|
+
from ummaya.evidence.source_provenance_redaction import redact_source_text
|
|
46
|
+
from ummaya.ipc.document_intent_normalization import (
|
|
47
|
+
_normalize_document_root_call_for_user_intent,
|
|
48
|
+
)
|
|
45
49
|
from ummaya.ipc.envelope import attach_envelope_span_attributes
|
|
46
50
|
from ummaya.ipc.frame_schema import (
|
|
47
51
|
ErrorFrame,
|
|
@@ -53,6 +57,7 @@ if TYPE_CHECKING:
|
|
|
53
57
|
from ummaya.session.manager import SessionManager
|
|
54
58
|
from ummaya.tools.executor import ToolExecutor
|
|
55
59
|
from ummaya.tools.registry import ToolRegistry
|
|
60
|
+
from ummaya.tools.routing import RouteDecision
|
|
56
61
|
|
|
57
62
|
logger = logging.getLogger(__name__)
|
|
58
63
|
|
|
@@ -74,6 +79,31 @@ _LEGACY_SCOPE_VERB_ALIASES: Final[dict[str, str]] = {
|
|
|
74
79
|
"submit": "send",
|
|
75
80
|
"verify": "check",
|
|
76
81
|
}
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class _BackendSecretRedactionFilter(logging.Filter):
|
|
85
|
+
def filter(self, record: logging.LogRecord) -> bool:
|
|
86
|
+
record.msg = self._redact_log_value(record.msg)
|
|
87
|
+
if isinstance(record.args, tuple):
|
|
88
|
+
record.args = tuple(self._redact_log_value(arg) for arg in record.args)
|
|
89
|
+
elif isinstance(record.args, dict):
|
|
90
|
+
record.args = {key: self._redact_log_value(value) for key, value in record.args.items()}
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
@staticmethod
|
|
94
|
+
def _redact_log_value(value: object) -> object:
|
|
95
|
+
if isinstance(value, str):
|
|
96
|
+
redacted, _categories = redact_source_text(value)
|
|
97
|
+
return redacted if redacted is not None else ""
|
|
98
|
+
if isinstance(value, (bool, int, float)) or value is None:
|
|
99
|
+
return value
|
|
100
|
+
text = str(value)
|
|
101
|
+
redacted, categories = redact_source_text(text)
|
|
102
|
+
if categories or redacted != text:
|
|
103
|
+
return redacted if redacted is not None else ""
|
|
104
|
+
return value
|
|
105
|
+
|
|
106
|
+
|
|
77
107
|
_CANONICAL_SCOPE_ALIASES: Final[dict[str, str]] = {
|
|
78
108
|
"find:mock_lookup_module_hometax_simplified": "find:hometax.simplified",
|
|
79
109
|
"find:mock.lookup_module_hometax_simplified": "find:hometax.simplified",
|
|
@@ -266,6 +296,11 @@ def _should_append_tui_tool_to_llm_tools(
|
|
|
266
296
|
return True
|
|
267
297
|
|
|
268
298
|
|
|
299
|
+
def _is_local_document_harness_tool(tool: object) -> bool:
|
|
300
|
+
endpoint = getattr(tool, "endpoint", "")
|
|
301
|
+
return isinstance(endpoint, str) and endpoint.startswith("local://document-harness/")
|
|
302
|
+
|
|
303
|
+
|
|
269
304
|
def _normalize_root_primitive_adapter_envelope(
|
|
270
305
|
fname: str,
|
|
271
306
|
args_obj: dict[str, object],
|
|
@@ -871,6 +906,80 @@ _KMA_ANALYSIS_MAP_USER_QUERY_RE: Final = re.compile(
|
|
|
871
906
|
r"(일기도|분석일기도|지도\s*자료|비구름|바람\s*흐름|synoptic|weather\s*chart)",
|
|
872
907
|
re.IGNORECASE,
|
|
873
908
|
)
|
|
909
|
+
_DOCUMENT_WRITE_REQUEST_RE: Final = re.compile(
|
|
910
|
+
r"(작성|수정|편집|채우|채워|입력|변경|저장|write|edit|fill|apply|save)",
|
|
911
|
+
re.IGNORECASE,
|
|
912
|
+
)
|
|
913
|
+
_DOCUMENT_REVIEW_REQUEST_RE: Final = re.compile(
|
|
914
|
+
r"(diff|compact|변경사항|렌더|미리보기|render|viewport|page)",
|
|
915
|
+
re.IGNORECASE,
|
|
916
|
+
)
|
|
917
|
+
_DOCUMENT_DIFF_ONLY_FINAL_REQUEST_RE: Final = re.compile(
|
|
918
|
+
r"(실제(?:로)?\s*바뀐\s*내용만|바뀐\s*내용만|변경된\s*부분만|변경사항만|"
|
|
919
|
+
r"actual\s+changed\s+content\s+only|only\s+changed)",
|
|
920
|
+
re.IGNORECASE,
|
|
921
|
+
)
|
|
922
|
+
_DOCUMENT_ARTIFACT_ID_RE: Final = re.compile(
|
|
923
|
+
r"(?:^|[\s\"'`(])(?:artifact_id|artifact\s*id|artifact|아티팩트)?\s*"
|
|
924
|
+
r"((?:source|working|derivative|render|export|viewport)-[A-Za-z0-9][A-Za-z0-9_.-]{0,127})"
|
|
925
|
+
r"(?=$|[^A-Za-z0-9_.-])",
|
|
926
|
+
re.IGNORECASE,
|
|
927
|
+
)
|
|
928
|
+
_DOCUMENT_MUTATION_TOOL_IDS: Final[frozenset[str]] = frozenset(
|
|
929
|
+
{"document_apply_fill", "document_apply_style"}
|
|
930
|
+
)
|
|
931
|
+
_DOCUMENT_FINAL_ANSWER_OVERCLAIM_MARKERS: Final[frozenset[str]] = frozenset(
|
|
932
|
+
{
|
|
933
|
+
"활동 내용",
|
|
934
|
+
"주요 성과",
|
|
935
|
+
"문제점",
|
|
936
|
+
"개선사항",
|
|
937
|
+
"다음 주 계획",
|
|
938
|
+
"향후 계획",
|
|
939
|
+
"개발 활동",
|
|
940
|
+
"시각적 비교 기능",
|
|
941
|
+
"구현 완료",
|
|
942
|
+
"UMMAYA Phase",
|
|
943
|
+
"도구 결과",
|
|
944
|
+
"작성하겠습니다",
|
|
945
|
+
"작성해 드리겠습니다",
|
|
946
|
+
"보여드리겠습니다",
|
|
947
|
+
"확인해보겠습니다",
|
|
948
|
+
"구성하고",
|
|
949
|
+
"작성하여",
|
|
950
|
+
"활동일지로 작성",
|
|
951
|
+
"주간 활동일지",
|
|
952
|
+
"다음 주차 활동일지",
|
|
953
|
+
"다음 주 활동 계획",
|
|
954
|
+
"활동 계획",
|
|
955
|
+
"주요 일정",
|
|
956
|
+
"주요 변경사항",
|
|
957
|
+
"변경 요약",
|
|
958
|
+
"도큐먼트 하네스",
|
|
959
|
+
"CLI 툴 체인",
|
|
960
|
+
"품질 검증",
|
|
961
|
+
"파이프라인 최적화",
|
|
962
|
+
"시각적 diff",
|
|
963
|
+
"시각적 차이 비교",
|
|
964
|
+
"렌더링 아티팩트",
|
|
965
|
+
"아티팩트도 생성",
|
|
966
|
+
"아티팩트 생성",
|
|
967
|
+
"저장되었습니다",
|
|
968
|
+
"저장 완료",
|
|
969
|
+
"성공적으로 저장",
|
|
970
|
+
"업데이트가 완료되었습니다",
|
|
971
|
+
"표시되어 있습니다",
|
|
972
|
+
"표시하겠습니다",
|
|
973
|
+
"확인하실 수 있습니다",
|
|
974
|
+
"changes in the TUI",
|
|
975
|
+
"준비되었습니다",
|
|
976
|
+
"📋",
|
|
977
|
+
"📅",
|
|
978
|
+
"🔄",
|
|
979
|
+
"📊",
|
|
980
|
+
"| 항목 |",
|
|
981
|
+
}
|
|
982
|
+
)
|
|
874
983
|
_KMA_ANALYSIS_TOOL_IDS: Final[frozenset[str]] = frozenset(
|
|
875
984
|
{
|
|
876
985
|
"kma_apihub_url_high_resolution_grid_point",
|
|
@@ -883,7 +992,8 @@ _PPS_BID_USER_QUERY_RE: Final = re.compile(
|
|
|
883
992
|
re.IGNORECASE,
|
|
884
993
|
)
|
|
885
994
|
_AIRKOREA_USER_QUERY_RE: Final = re.compile(
|
|
886
|
-
r"(
|
|
995
|
+
r"(미세먼지|초미세먼지|초미세|대기질|대기오염|공기질|마스크|"
|
|
996
|
+
r"pm\s*2\.?5|pm\s*10|air\s*korea|airkorea|air\s*quality|airquality)",
|
|
887
997
|
re.IGNORECASE,
|
|
888
998
|
)
|
|
889
999
|
_TAGO_BUS_USER_QUERY_RE: Final = re.compile(
|
|
@@ -911,29 +1021,24 @@ def _initial_concrete_tool_choice_for_query(
|
|
|
911
1021
|
) -> str | None:
|
|
912
1022
|
"""Force direct first calls only for unambiguous single-adapter lookups."""
|
|
913
1023
|
available = set(available_tool_names)
|
|
1024
|
+
return _document_tool_choice_for_query(user_query, available)
|
|
1025
|
+
|
|
1026
|
+
|
|
1027
|
+
def _document_tool_choice_for_query(
|
|
1028
|
+
user_query: str,
|
|
1029
|
+
available: set[str],
|
|
1030
|
+
) -> str | None:
|
|
1031
|
+
"""Force unambiguous local document turns through the single document primitive."""
|
|
1032
|
+
from ummaya.tools.search import is_document_harness_query # noqa: PLC0415
|
|
1033
|
+
|
|
914
1034
|
if (
|
|
915
|
-
|
|
916
|
-
and
|
|
1035
|
+
_DOCUMENT_ARTIFACT_ID_RE.search(user_query)
|
|
1036
|
+
and _DOCUMENT_REVIEW_REQUEST_RE.search(user_query)
|
|
1037
|
+
and "document" in available
|
|
917
1038
|
):
|
|
918
|
-
return
|
|
919
|
-
if
|
|
920
|
-
|
|
921
|
-
re.search(r"(김포|gimpo|rkss)", user_query, re.IGNORECASE)
|
|
922
|
-
and re.search(r"(amos|활주로|rvr|runway|매분)", user_query, re.IGNORECASE)
|
|
923
|
-
and "kma_apihub_url_air_amos_minute" in available
|
|
924
|
-
):
|
|
925
|
-
return "kma_apihub_url_air_amos_minute"
|
|
926
|
-
if "kma_apihub_url_air_metar_decoded" in available:
|
|
927
|
-
return "kma_apihub_url_air_metar_decoded"
|
|
928
|
-
if _PPS_BID_USER_QUERY_RE.search(user_query) and _PPS_BID_TOOL_ID in available:
|
|
929
|
-
return _PPS_BID_TOOL_ID
|
|
930
|
-
if _AIRKOREA_USER_QUERY_RE.search(user_query) and _AIRKOREA_TOOL_ID in available:
|
|
931
|
-
return _AIRKOREA_TOOL_ID
|
|
932
|
-
if _TAGO_BUS_USER_QUERY_RE.search(user_query):
|
|
933
|
-
if _TAGO_ROUTE_NO_RE.search(user_query) and "tago_bus_route_search" in available:
|
|
934
|
-
return "tago_bus_route_search"
|
|
935
|
-
if "tago_bus_station_search" in available:
|
|
936
|
-
return "tago_bus_station_search"
|
|
1039
|
+
return "document"
|
|
1040
|
+
if is_document_harness_query(user_query) and "document" in available:
|
|
1041
|
+
return "document"
|
|
937
1042
|
return None
|
|
938
1043
|
|
|
939
1044
|
|
|
@@ -1036,6 +1141,7 @@ def _conversation_has_successful_any_primitive_result(llm_messages: list[Any]) -
|
|
|
1036
1141
|
or _conversation_has_successful_primitive_any_tool(llm_messages, primitive="locate")
|
|
1037
1142
|
or _conversation_has_successful_primitive_any_tool(llm_messages, primitive="check")
|
|
1038
1143
|
or _conversation_has_successful_primitive_any_tool(llm_messages, primitive="send")
|
|
1144
|
+
or _conversation_has_successful_primitive_any_tool(llm_messages, primitive="document")
|
|
1039
1145
|
)
|
|
1040
1146
|
|
|
1041
1147
|
|
|
@@ -1653,6 +1759,80 @@ def _payload_dict_is_error_like(payload: dict[str, object]) -> bool:
|
|
|
1653
1759
|
return isinstance(error, str) and bool(error)
|
|
1654
1760
|
|
|
1655
1761
|
|
|
1762
|
+
def _message_role(msg: Any) -> object:
|
|
1763
|
+
"""Return a transcript message role across SDK and dict shapes."""
|
|
1764
|
+
return getattr(msg, "role", None) or (msg.get("role") if isinstance(msg, dict) else None)
|
|
1765
|
+
|
|
1766
|
+
|
|
1767
|
+
def _message_content(msg: Any) -> object:
|
|
1768
|
+
"""Return a transcript message content across SDK and dict shapes."""
|
|
1769
|
+
content = getattr(msg, "content", None)
|
|
1770
|
+
if content is None and isinstance(msg, dict):
|
|
1771
|
+
content = msg.get("content")
|
|
1772
|
+
return content
|
|
1773
|
+
|
|
1774
|
+
|
|
1775
|
+
def _decode_tool_result_payload_content(content: object) -> object | None:
|
|
1776
|
+
"""Decode the payload stored inside a tool_result message/content part."""
|
|
1777
|
+
if isinstance(content, str):
|
|
1778
|
+
try:
|
|
1779
|
+
payload: object = _stdlib_json.loads(content)
|
|
1780
|
+
return payload
|
|
1781
|
+
except _stdlib_json.JSONDecodeError:
|
|
1782
|
+
return None
|
|
1783
|
+
if isinstance(content, (dict, list)):
|
|
1784
|
+
return content
|
|
1785
|
+
return None
|
|
1786
|
+
|
|
1787
|
+
|
|
1788
|
+
def _iter_tool_result_payloads(msg: Any) -> list[tuple[str | None, str | None, object]]:
|
|
1789
|
+
"""Return tool_result payloads from OpenAI tool-role or CC user-role shapes."""
|
|
1790
|
+
role = _message_role(msg)
|
|
1791
|
+
if role == "tool":
|
|
1792
|
+
content = _message_content(msg)
|
|
1793
|
+
payload = _decode_tool_result_payload_content(content)
|
|
1794
|
+
if payload is None:
|
|
1795
|
+
return []
|
|
1796
|
+
call_id = getattr(msg, "tool_call_id", None) or (
|
|
1797
|
+
msg.get("tool_call_id") if isinstance(msg, dict) else None
|
|
1798
|
+
)
|
|
1799
|
+
name = getattr(msg, "name", None) or (msg.get("name") if isinstance(msg, dict) else None)
|
|
1800
|
+
return [
|
|
1801
|
+
(
|
|
1802
|
+
call_id if isinstance(call_id, str) else None,
|
|
1803
|
+
name if isinstance(name, str) else None,
|
|
1804
|
+
payload,
|
|
1805
|
+
)
|
|
1806
|
+
]
|
|
1807
|
+
if role != "user":
|
|
1808
|
+
return []
|
|
1809
|
+
content = _message_content(msg)
|
|
1810
|
+
if not isinstance(content, list):
|
|
1811
|
+
return []
|
|
1812
|
+
payloads: list[tuple[str | None, str | None, object]] = []
|
|
1813
|
+
for item in content:
|
|
1814
|
+
if not isinstance(item, dict) or item.get("type") != "tool_result":
|
|
1815
|
+
continue
|
|
1816
|
+
payload = _decode_tool_result_payload_content(item.get("content"))
|
|
1817
|
+
if payload is None:
|
|
1818
|
+
continue
|
|
1819
|
+
call_id = item.get("tool_use_id")
|
|
1820
|
+
payloads.append((call_id if isinstance(call_id, str) else None, None, payload))
|
|
1821
|
+
return payloads
|
|
1822
|
+
|
|
1823
|
+
|
|
1824
|
+
def _message_is_tool_result_only(msg: Any) -> bool:
|
|
1825
|
+
"""Return True for CC-style user messages that only carry tool_result blocks."""
|
|
1826
|
+
if _message_role(msg) != "user":
|
|
1827
|
+
return False
|
|
1828
|
+
content = _message_content(msg)
|
|
1829
|
+
return (
|
|
1830
|
+
isinstance(content, list)
|
|
1831
|
+
and bool(content)
|
|
1832
|
+
and all(isinstance(item, dict) and item.get("type") == "tool_result" for item in content)
|
|
1833
|
+
)
|
|
1834
|
+
|
|
1835
|
+
|
|
1656
1836
|
def _tool_result_payload_is_error(payload: object) -> bool:
|
|
1657
1837
|
"""Return True for structured tool-result payloads that are errors."""
|
|
1658
1838
|
if not isinstance(payload, dict):
|
|
@@ -1665,6 +1845,224 @@ def _tool_result_payload_is_error(payload: object) -> bool:
|
|
|
1665
1845
|
)
|
|
1666
1846
|
|
|
1667
1847
|
|
|
1848
|
+
def _payload_has_successful_document_tool_id(
|
|
1849
|
+
payload: object,
|
|
1850
|
+
tool_ids: frozenset[str],
|
|
1851
|
+
) -> bool:
|
|
1852
|
+
"""Return True when a structured tool result contains a successful document tool."""
|
|
1853
|
+
if isinstance(payload, list):
|
|
1854
|
+
return any(_payload_has_successful_document_tool_id(item, tool_ids) for item in payload)
|
|
1855
|
+
if not isinstance(payload, dict):
|
|
1856
|
+
return False
|
|
1857
|
+
tool_id = payload.get("tool_id")
|
|
1858
|
+
if isinstance(tool_id, str) and tool_id in tool_ids:
|
|
1859
|
+
status = payload.get("status")
|
|
1860
|
+
status_text = str(status).lower() if status is not None else "ok"
|
|
1861
|
+
if status_text in {"ok", "succeeded", "completed", "ready"} and not (
|
|
1862
|
+
_tool_result_payload_is_error(payload)
|
|
1863
|
+
):
|
|
1864
|
+
return True
|
|
1865
|
+
return any(
|
|
1866
|
+
_payload_has_successful_document_tool_id(value, tool_ids) for value in payload.values()
|
|
1867
|
+
)
|
|
1868
|
+
|
|
1869
|
+
|
|
1870
|
+
def _conversation_has_successful_document_tool_result(
|
|
1871
|
+
llm_messages: list[Any],
|
|
1872
|
+
*,
|
|
1873
|
+
tool_ids: frozenset[str],
|
|
1874
|
+
) -> bool:
|
|
1875
|
+
"""Return True when a concrete document tool has a successful tool_result."""
|
|
1876
|
+
return _conversation_has_successful_document_tool_result_after(
|
|
1877
|
+
llm_messages,
|
|
1878
|
+
tool_ids=tool_ids,
|
|
1879
|
+
after_index=-1,
|
|
1880
|
+
)
|
|
1881
|
+
|
|
1882
|
+
|
|
1883
|
+
def _conversation_has_successful_document_tool_result_after(
|
|
1884
|
+
llm_messages: list[Any],
|
|
1885
|
+
*,
|
|
1886
|
+
tool_ids: frozenset[str],
|
|
1887
|
+
after_index: int,
|
|
1888
|
+
) -> bool:
|
|
1889
|
+
"""Return True when a successful document tool_result appears after an index."""
|
|
1890
|
+
start_index = max(0, after_index + 1)
|
|
1891
|
+
for msg in llm_messages[start_index:]:
|
|
1892
|
+
for _, _, payload in _iter_tool_result_payloads(msg):
|
|
1893
|
+
if _payload_has_successful_document_tool_id(payload, tool_ids):
|
|
1894
|
+
return True
|
|
1895
|
+
return False
|
|
1896
|
+
|
|
1897
|
+
|
|
1898
|
+
def _conversation_has_successful_document_diff_result_after(
|
|
1899
|
+
llm_messages: list[Any],
|
|
1900
|
+
*,
|
|
1901
|
+
after_index: int,
|
|
1902
|
+
) -> bool:
|
|
1903
|
+
"""Return True when a successful document mutation/render diff appears."""
|
|
1904
|
+
start_index = max(0, after_index + 1)
|
|
1905
|
+
for msg in llm_messages[start_index:]:
|
|
1906
|
+
for _, _, payload in _iter_tool_result_payloads(msg):
|
|
1907
|
+
document_result = _extract_successful_document_result_payload(payload)
|
|
1908
|
+
if document_result is not None and _document_diff_changes(document_result):
|
|
1909
|
+
return True
|
|
1910
|
+
return False
|
|
1911
|
+
|
|
1912
|
+
|
|
1913
|
+
def _conversation_has_successful_document_completion_result_after(
|
|
1914
|
+
llm_messages: list[Any],
|
|
1915
|
+
*,
|
|
1916
|
+
after_index: int,
|
|
1917
|
+
) -> bool:
|
|
1918
|
+
"""Return True when the latest turn has a completed document/review result."""
|
|
1919
|
+
start_index = max(0, after_index + 1)
|
|
1920
|
+
for msg in llm_messages[start_index:]:
|
|
1921
|
+
for _, _, payload in _iter_tool_result_payloads(msg):
|
|
1922
|
+
if _extract_successful_document_completion_payload(payload) is not None:
|
|
1923
|
+
return True
|
|
1924
|
+
return False
|
|
1925
|
+
|
|
1926
|
+
|
|
1927
|
+
def _extract_successful_document_completion_payload(payload: object) -> dict[str, object] | None:
|
|
1928
|
+
"""Return a successful document completion payload, excluding inspect-only results."""
|
|
1929
|
+
if isinstance(payload, list):
|
|
1930
|
+
return _extract_successful_document_completion_from_sequence(payload)
|
|
1931
|
+
if isinstance(payload, dict):
|
|
1932
|
+
return _extract_successful_document_completion_from_dict(payload)
|
|
1933
|
+
return None
|
|
1934
|
+
|
|
1935
|
+
|
|
1936
|
+
def _extract_successful_document_completion_from_sequence(
|
|
1937
|
+
payload: list[object],
|
|
1938
|
+
) -> dict[str, object] | None:
|
|
1939
|
+
for item in reversed(payload):
|
|
1940
|
+
result = _extract_successful_document_completion_payload(item)
|
|
1941
|
+
if result is not None:
|
|
1942
|
+
return result
|
|
1943
|
+
return None
|
|
1944
|
+
|
|
1945
|
+
|
|
1946
|
+
def _extract_successful_document_completion_from_dict(
|
|
1947
|
+
payload: dict[str, object],
|
|
1948
|
+
) -> dict[str, object] | None:
|
|
1949
|
+
if _tool_result_payload_is_error(payload):
|
|
1950
|
+
return None
|
|
1951
|
+
|
|
1952
|
+
direct = _direct_successful_document_completion_payload(payload)
|
|
1953
|
+
if direct is not None:
|
|
1954
|
+
return direct
|
|
1955
|
+
|
|
1956
|
+
result = payload.get("result")
|
|
1957
|
+
if isinstance(result, dict):
|
|
1958
|
+
nested = _extract_successful_document_completion_payload(result)
|
|
1959
|
+
if nested is not None:
|
|
1960
|
+
return nested
|
|
1961
|
+
return _extract_successful_document_completion_from_sequence(list(payload.values()))
|
|
1962
|
+
|
|
1963
|
+
|
|
1964
|
+
def _direct_successful_document_completion_payload(
|
|
1965
|
+
payload: dict[str, object],
|
|
1966
|
+
) -> dict[str, object] | None:
|
|
1967
|
+
tool_id = payload.get("tool_id")
|
|
1968
|
+
if not isinstance(tool_id, str) or tool_id not in {"document", "document_render"}:
|
|
1969
|
+
return None
|
|
1970
|
+
status = payload.get("status")
|
|
1971
|
+
status_text = str(status).lower() if status is not None else "ok"
|
|
1972
|
+
if status_text not in {"ok", "succeeded", "completed", "ready"}:
|
|
1973
|
+
return None
|
|
1974
|
+
if tool_id == "document" and _document_result_is_inspect_only(payload):
|
|
1975
|
+
return None
|
|
1976
|
+
return payload
|
|
1977
|
+
|
|
1978
|
+
|
|
1979
|
+
def _document_result_is_inspect_only(payload: dict[str, object]) -> bool:
|
|
1980
|
+
"""Return True for document primitive results that only inspected a file."""
|
|
1981
|
+
if _document_diff_changes(payload):
|
|
1982
|
+
return False
|
|
1983
|
+
diff = payload.get("diff")
|
|
1984
|
+
if diff is None and isinstance(payload.get("extraction"), dict):
|
|
1985
|
+
return True
|
|
1986
|
+
if diff is None and payload.get("render_artifacts") == []:
|
|
1987
|
+
artifact_refs = payload.get("artifact_refs")
|
|
1988
|
+
if isinstance(artifact_refs, list) and artifact_refs:
|
|
1989
|
+
has_only_source_refs = all(
|
|
1990
|
+
isinstance(ref, str) and ref.startswith("source-") for ref in artifact_refs
|
|
1991
|
+
)
|
|
1992
|
+
if has_only_source_refs:
|
|
1993
|
+
return True
|
|
1994
|
+
summary = str(payload.get("text_summary") or "").casefold()
|
|
1995
|
+
return (
|
|
1996
|
+
"inspection completed through the document primitive" in summary
|
|
1997
|
+
or "document inspection completed" in summary
|
|
1998
|
+
or "document extraction completed" in summary
|
|
1999
|
+
)
|
|
2000
|
+
|
|
2001
|
+
|
|
2002
|
+
def _latest_user_message_index(llm_messages: list[Any]) -> int:
|
|
2003
|
+
"""Return the latest user-message index in the LLM transcript."""
|
|
2004
|
+
for index in range(len(llm_messages) - 1, -1, -1):
|
|
2005
|
+
msg = llm_messages[index]
|
|
2006
|
+
if _message_role(msg) == "user" and not _message_is_tool_result_only(msg):
|
|
2007
|
+
return index
|
|
2008
|
+
return -1
|
|
2009
|
+
|
|
2010
|
+
|
|
2011
|
+
def _check_document_workflow_terminated_without_required_tool(
|
|
2012
|
+
llm_messages: list[Any],
|
|
2013
|
+
latest_user_utt: str,
|
|
2014
|
+
*,
|
|
2015
|
+
candidate_final_answer: str = "",
|
|
2016
|
+
) -> dict[str, str] | None:
|
|
2017
|
+
"""Return the missing document tool when a document workflow tries to stop early."""
|
|
2018
|
+
from ummaya.tools.search import is_document_harness_query # noqa: PLC0415
|
|
2019
|
+
|
|
2020
|
+
text_for_intent = f"{latest_user_utt}\n{candidate_final_answer}"
|
|
2021
|
+
latest_user_index = _latest_user_message_index(llm_messages)
|
|
2022
|
+
has_document_workflow_activity = _conversation_has_successful_document_tool_result(
|
|
2023
|
+
llm_messages,
|
|
2024
|
+
tool_ids=frozenset(
|
|
2025
|
+
{
|
|
2026
|
+
"document",
|
|
2027
|
+
"document_render",
|
|
2028
|
+
"document_inspect",
|
|
2029
|
+
"document_extract",
|
|
2030
|
+
"document_form_schema",
|
|
2031
|
+
"document_copy_for_edit",
|
|
2032
|
+
"document_apply_fill",
|
|
2033
|
+
"document_apply_style",
|
|
2034
|
+
"document_validate_public_form",
|
|
2035
|
+
"document_save",
|
|
2036
|
+
}
|
|
2037
|
+
),
|
|
2038
|
+
)
|
|
2039
|
+
if not is_document_harness_query(latest_user_utt) and not has_document_workflow_activity:
|
|
2040
|
+
return None
|
|
2041
|
+
wants_write = bool(_DOCUMENT_WRITE_REQUEST_RE.search(text_for_intent))
|
|
2042
|
+
wants_review = bool(_DOCUMENT_REVIEW_REQUEST_RE.search(text_for_intent))
|
|
2043
|
+
if not wants_write and not wants_review:
|
|
2044
|
+
return None
|
|
2045
|
+
has_document_result_for_latest_request = (
|
|
2046
|
+
_conversation_has_successful_document_completion_result_after(
|
|
2047
|
+
llm_messages,
|
|
2048
|
+
after_index=latest_user_index,
|
|
2049
|
+
)
|
|
2050
|
+
)
|
|
2051
|
+
if has_document_result_for_latest_request:
|
|
2052
|
+
return None
|
|
2053
|
+
return {
|
|
2054
|
+
"tool_id": "document",
|
|
2055
|
+
"message": (
|
|
2056
|
+
"Document workflow request has no successful document primitive result "
|
|
2057
|
+
"for the latest user turn. Do NOT answer from intended edits or "
|
|
2058
|
+
"fabricate compact diff text. Call document once with the document "
|
|
2059
|
+
"locator, requested operation, instruction, and any inferred patches; "
|
|
2060
|
+
"the runtime will inspect, copy, mutate, render, and return the "
|
|
2061
|
+
"automatic compact diff."
|
|
2062
|
+
),
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
|
|
1668
2066
|
def _lookup_call_ids_for_tool(
|
|
1669
2067
|
llm_messages: list[Any],
|
|
1670
2068
|
*,
|
|
@@ -1708,24 +2106,10 @@ def _tool_result_payload_for_call(
|
|
|
1708
2106
|
matching_call_ids: set[str],
|
|
1709
2107
|
) -> object | None:
|
|
1710
2108
|
"""Parse a lookup tool-result message when it matches one of call IDs."""
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
msg.get("tool_call_id") if isinstance(msg, dict) else None
|
|
1716
|
-
)
|
|
1717
|
-
if not isinstance(call_id, str) or call_id not in matching_call_ids:
|
|
1718
|
-
return None
|
|
1719
|
-
content = getattr(msg, "content", None) or (
|
|
1720
|
-
msg.get("content") if isinstance(msg, dict) else None
|
|
1721
|
-
)
|
|
1722
|
-
if not isinstance(content, str):
|
|
1723
|
-
return None
|
|
1724
|
-
try:
|
|
1725
|
-
payload: object = _stdlib_json.loads(content)
|
|
1726
|
-
return payload
|
|
1727
|
-
except _stdlib_json.JSONDecodeError:
|
|
1728
|
-
return None
|
|
2109
|
+
for call_id, _, payload in _iter_tool_result_payloads(msg):
|
|
2110
|
+
if isinstance(call_id, str) and call_id in matching_call_ids:
|
|
2111
|
+
return payload
|
|
2112
|
+
return None
|
|
1729
2113
|
|
|
1730
2114
|
|
|
1731
2115
|
def _conversation_has_successful_lookup(
|
|
@@ -1791,24 +2175,11 @@ def _tool_result_payload_for_primitive_call(
|
|
|
1791
2175
|
matching_call_ids: set[str],
|
|
1792
2176
|
) -> object | None:
|
|
1793
2177
|
"""Parse a primitive tool-result message when it matches one of call IDs."""
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
)
|
|
1800
|
-
if not isinstance(call_id, str) or call_id not in matching_call_ids:
|
|
1801
|
-
return None
|
|
1802
|
-
content = getattr(msg, "content", None) or (
|
|
1803
|
-
msg.get("content") if isinstance(msg, dict) else None
|
|
1804
|
-
)
|
|
1805
|
-
if not isinstance(content, str):
|
|
1806
|
-
return None
|
|
1807
|
-
try:
|
|
1808
|
-
payload: object = _stdlib_json.loads(content)
|
|
1809
|
-
return payload
|
|
1810
|
-
except _stdlib_json.JSONDecodeError:
|
|
1811
|
-
return None
|
|
2178
|
+
_ = primitive
|
|
2179
|
+
for call_id, _, payload in _iter_tool_result_payloads(msg):
|
|
2180
|
+
if isinstance(call_id, str) and call_id in matching_call_ids:
|
|
2181
|
+
return payload
|
|
2182
|
+
return None
|
|
1812
2183
|
|
|
1813
2184
|
|
|
1814
2185
|
def _tool_result_payload_for_primitive(
|
|
@@ -1823,24 +2194,17 @@ def _tool_result_payload_for_primitive(
|
|
|
1823
2194
|
resolved state of the most recent primitive invocation, not a specific
|
|
1824
2195
|
call handle.
|
|
1825
2196
|
"""
|
|
1826
|
-
|
|
1827
|
-
if role != "tool":
|
|
1828
|
-
return None
|
|
1829
|
-
name = getattr(msg, "name", None) or (msg.get("name") if isinstance(msg, dict) else None)
|
|
1830
|
-
content = getattr(msg, "content", None) or (
|
|
1831
|
-
msg.get("content") if isinstance(msg, dict) else None
|
|
1832
|
-
)
|
|
1833
|
-
if not isinstance(content, str):
|
|
1834
|
-
return None
|
|
1835
|
-
try:
|
|
1836
|
-
payload: object = _stdlib_json.loads(content)
|
|
2197
|
+
for _, name, payload in _iter_tool_result_payloads(msg):
|
|
1837
2198
|
if name == primitive:
|
|
1838
2199
|
return payload
|
|
1839
2200
|
if isinstance(payload, dict) and payload.get("kind") == primitive:
|
|
1840
2201
|
return payload
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
2202
|
+
result = _primitive_payload_result_dict(payload)
|
|
2203
|
+
if primitive == "document" and result is not None:
|
|
2204
|
+
tool_id = result.get("tool_id")
|
|
2205
|
+
if tool_id in {"document", "document_render"}:
|
|
2206
|
+
return payload
|
|
2207
|
+
return None
|
|
1844
2208
|
|
|
1845
2209
|
|
|
1846
2210
|
def _primitive_payload_is_success(payload: object, *, primitive: str) -> bool:
|
|
@@ -1854,6 +2218,10 @@ def _primitive_payload_is_success(payload: object, *, primitive: str) -> bool:
|
|
|
1854
2218
|
if isinstance(result, dict) and result.get("status") == "succeeded":
|
|
1855
2219
|
return True
|
|
1856
2220
|
return payload.get("status") == "succeeded"
|
|
2221
|
+
if primitive == "document":
|
|
2222
|
+
if isinstance(result, dict):
|
|
2223
|
+
return result.get("status") == "ok"
|
|
2224
|
+
return payload.get("status") == "ok"
|
|
1857
2225
|
return True
|
|
1858
2226
|
|
|
1859
2227
|
|
|
@@ -2172,7 +2540,7 @@ def _latest_successful_primitive_observation(
|
|
|
2172
2540
|
)
|
|
2173
2541
|
primitive: object = tool_message_name
|
|
2174
2542
|
payload: object | None = None
|
|
2175
|
-
if primitive not in {"find", "locate", "check", "send"}:
|
|
2543
|
+
if primitive not in {"find", "locate", "check", "send", "document"}:
|
|
2176
2544
|
if not isinstance(content, str):
|
|
2177
2545
|
continue
|
|
2178
2546
|
try:
|
|
@@ -2182,7 +2550,7 @@ def _latest_successful_primitive_observation(
|
|
|
2182
2550
|
if not isinstance(parsed_payload, dict):
|
|
2183
2551
|
continue
|
|
2184
2552
|
primitive = parsed_payload.get("kind")
|
|
2185
|
-
if primitive not in {"find", "locate", "check", "send"}:
|
|
2553
|
+
if primitive not in {"find", "locate", "check", "send", "document"}:
|
|
2186
2554
|
continue
|
|
2187
2555
|
payload = parsed_payload
|
|
2188
2556
|
if payload is None:
|
|
@@ -2200,6 +2568,183 @@ def _latest_successful_primitive_observation(
|
|
|
2200
2568
|
return None
|
|
2201
2569
|
|
|
2202
2570
|
|
|
2571
|
+
def _latest_successful_document_result(llm_messages: list[Any]) -> dict[str, object] | None:
|
|
2572
|
+
"""Return the latest successful document primitive result payload."""
|
|
2573
|
+
for msg in reversed(llm_messages):
|
|
2574
|
+
payload = _tool_result_payload_for_primitive(msg, primitive="document")
|
|
2575
|
+
if payload is not None and _primitive_payload_is_success(payload, primitive="document"):
|
|
2576
|
+
result = _primitive_payload_result_dict(payload)
|
|
2577
|
+
if result is not None:
|
|
2578
|
+
return result
|
|
2579
|
+
if isinstance(payload, dict):
|
|
2580
|
+
return cast("dict[str, object]", payload)
|
|
2581
|
+
role = getattr(msg, "role", None) or (msg.get("role") if isinstance(msg, dict) else None)
|
|
2582
|
+
if role != "tool":
|
|
2583
|
+
continue
|
|
2584
|
+
content = getattr(msg, "content", None)
|
|
2585
|
+
if content is None and isinstance(msg, dict):
|
|
2586
|
+
content = msg.get("content")
|
|
2587
|
+
parsed_payload: object = None
|
|
2588
|
+
if isinstance(content, str):
|
|
2589
|
+
try:
|
|
2590
|
+
parsed_payload = _stdlib_json.loads(content)
|
|
2591
|
+
except _stdlib_json.JSONDecodeError:
|
|
2592
|
+
continue
|
|
2593
|
+
else:
|
|
2594
|
+
parsed_payload = content
|
|
2595
|
+
document_result = _extract_successful_document_result_payload(parsed_payload)
|
|
2596
|
+
if document_result is not None:
|
|
2597
|
+
return document_result
|
|
2598
|
+
return None
|
|
2599
|
+
|
|
2600
|
+
|
|
2601
|
+
def _extract_successful_document_result_payload(payload: object) -> dict[str, object] | None:
|
|
2602
|
+
"""Return a successful document result from direct, wrapped, or nested payloads."""
|
|
2603
|
+
if isinstance(payload, list):
|
|
2604
|
+
return _extract_successful_document_result_from_sequence(payload)
|
|
2605
|
+
if isinstance(payload, dict):
|
|
2606
|
+
return _extract_successful_document_result_from_dict(payload)
|
|
2607
|
+
return None
|
|
2608
|
+
|
|
2609
|
+
|
|
2610
|
+
def _extract_successful_document_result_from_sequence(
|
|
2611
|
+
payload: list[object],
|
|
2612
|
+
) -> dict[str, object] | None:
|
|
2613
|
+
"""Return the last successful document result from a payload sequence."""
|
|
2614
|
+
for item in reversed(payload):
|
|
2615
|
+
document_result = _extract_successful_document_result_payload(item)
|
|
2616
|
+
if document_result is not None:
|
|
2617
|
+
return document_result
|
|
2618
|
+
return None
|
|
2619
|
+
|
|
2620
|
+
|
|
2621
|
+
def _extract_successful_document_result_from_dict(
|
|
2622
|
+
payload: dict[str, object],
|
|
2623
|
+
) -> dict[str, object] | None:
|
|
2624
|
+
"""Return a successful document result from a payload mapping."""
|
|
2625
|
+
if _tool_result_payload_is_error(payload):
|
|
2626
|
+
return None
|
|
2627
|
+
result = payload.get("result")
|
|
2628
|
+
if isinstance(result, dict):
|
|
2629
|
+
document_result = _extract_successful_document_result_payload(result)
|
|
2630
|
+
if document_result is not None:
|
|
2631
|
+
return document_result
|
|
2632
|
+
if _payload_is_document_result_with_diff(payload):
|
|
2633
|
+
return payload
|
|
2634
|
+
for nested in reversed(list(payload.values())):
|
|
2635
|
+
document_result = _extract_successful_document_result_payload(nested)
|
|
2636
|
+
if document_result is not None:
|
|
2637
|
+
return document_result
|
|
2638
|
+
return None
|
|
2639
|
+
|
|
2640
|
+
|
|
2641
|
+
def _payload_is_document_result_with_diff(payload: dict[str, object]) -> bool:
|
|
2642
|
+
"""Return True when a payload has the user-visible document diff contract."""
|
|
2643
|
+
diff = payload.get("diff")
|
|
2644
|
+
status = str(payload.get("status") or "ok").lower()
|
|
2645
|
+
return (
|
|
2646
|
+
isinstance(diff, dict)
|
|
2647
|
+
and isinstance(diff.get("changes"), list)
|
|
2648
|
+
and status in {"ok", "succeeded", "completed", "ready"}
|
|
2649
|
+
)
|
|
2650
|
+
|
|
2651
|
+
|
|
2652
|
+
def _document_diff_changes(result: dict[str, object]) -> list[dict[str, object]]:
|
|
2653
|
+
"""Return structured document diff changes from a document result."""
|
|
2654
|
+
diff = result.get("diff")
|
|
2655
|
+
if not isinstance(diff, dict):
|
|
2656
|
+
return []
|
|
2657
|
+
changes = diff.get("changes")
|
|
2658
|
+
if not isinstance(changes, list):
|
|
2659
|
+
return []
|
|
2660
|
+
return [change for change in changes if isinstance(change, dict)]
|
|
2661
|
+
|
|
2662
|
+
|
|
2663
|
+
def _document_result_allowed_claim_text(result: dict[str, object]) -> str:
|
|
2664
|
+
"""Build the bounded document-result text a final answer may claim from."""
|
|
2665
|
+
parts: list[str] = []
|
|
2666
|
+
for key in ("tool_id", "status", "text_summary"):
|
|
2667
|
+
value = result.get(key)
|
|
2668
|
+
if value is not None:
|
|
2669
|
+
parts.append(str(value))
|
|
2670
|
+
for change in _document_diff_changes(result):
|
|
2671
|
+
for key in (
|
|
2672
|
+
"change_id",
|
|
2673
|
+
"change_type",
|
|
2674
|
+
"display_label",
|
|
2675
|
+
"target_path",
|
|
2676
|
+
"before_value",
|
|
2677
|
+
"after_value",
|
|
2678
|
+
):
|
|
2679
|
+
value = change.get(key)
|
|
2680
|
+
if value is not None:
|
|
2681
|
+
parts.append(str(value))
|
|
2682
|
+
return "\n".join(parts)
|
|
2683
|
+
|
|
2684
|
+
|
|
2685
|
+
def _document_diff_only_final_answer(
|
|
2686
|
+
latest_user_utt: str,
|
|
2687
|
+
llm_messages: list[Any],
|
|
2688
|
+
) -> str | None:
|
|
2689
|
+
"""Build a diff-only final answer when the citizen explicitly asks for it."""
|
|
2690
|
+
if not _DOCUMENT_DIFF_ONLY_FINAL_REQUEST_RE.search(latest_user_utt):
|
|
2691
|
+
return None
|
|
2692
|
+
result = _latest_successful_document_result(llm_messages)
|
|
2693
|
+
if result is None:
|
|
2694
|
+
return None
|
|
2695
|
+
changes = _document_diff_changes(result)
|
|
2696
|
+
if not changes:
|
|
2697
|
+
return None
|
|
2698
|
+
lines = ["실제 변경된 내용:"]
|
|
2699
|
+
for change in changes:
|
|
2700
|
+
target_path = str(change.get("display_label") or change.get("target_path") or "document")
|
|
2701
|
+
before = str(change.get("before_value") or "")
|
|
2702
|
+
after = str(change.get("after_value") or "")
|
|
2703
|
+
lines.append(f"- {target_path}: {before} -> {after}")
|
|
2704
|
+
return "\n".join(lines)
|
|
2705
|
+
|
|
2706
|
+
|
|
2707
|
+
def _compact_claim_text(text: str) -> str:
|
|
2708
|
+
"""Normalize claim text for marker comparison without losing Korean terms."""
|
|
2709
|
+
return re.sub(r"\s+", "", text).casefold()
|
|
2710
|
+
|
|
2711
|
+
|
|
2712
|
+
def _final_answer_overclaims_document_edit(
|
|
2713
|
+
text: str,
|
|
2714
|
+
llm_messages: list[Any],
|
|
2715
|
+
) -> bool:
|
|
2716
|
+
"""Return True when a document final answer adds content absent from the diff."""
|
|
2717
|
+
if not text.strip():
|
|
2718
|
+
return False
|
|
2719
|
+
result = _latest_successful_document_result(llm_messages)
|
|
2720
|
+
if result is None:
|
|
2721
|
+
return False
|
|
2722
|
+
changes = _document_diff_changes(result)
|
|
2723
|
+
if not changes:
|
|
2724
|
+
return False
|
|
2725
|
+
nonempty_lines = [line.strip() for line in text.splitlines() if line.strip()]
|
|
2726
|
+
max_expected_lines = max(8, len(changes) * 4)
|
|
2727
|
+
if len(nonempty_lines) > max_expected_lines:
|
|
2728
|
+
return True
|
|
2729
|
+
allowed = _compact_claim_text(_document_result_allowed_claim_text(result))
|
|
2730
|
+
answer = _compact_claim_text(text)
|
|
2731
|
+
for change in changes:
|
|
2732
|
+
display_label = str(change.get("display_label") or "").strip()
|
|
2733
|
+
target_path = str(change.get("target_path") or "").strip()
|
|
2734
|
+
if not display_label or not target_path:
|
|
2735
|
+
continue
|
|
2736
|
+
if (
|
|
2737
|
+
_compact_claim_text(target_path) in answer
|
|
2738
|
+
and _compact_claim_text(display_label) not in answer
|
|
2739
|
+
):
|
|
2740
|
+
return True
|
|
2741
|
+
for marker in _DOCUMENT_FINAL_ANSWER_OVERCLAIM_MARKERS:
|
|
2742
|
+
marker_text = _compact_claim_text(marker)
|
|
2743
|
+
if marker_text in answer and marker_text not in allowed:
|
|
2744
|
+
return True
|
|
2745
|
+
return False
|
|
2746
|
+
|
|
2747
|
+
|
|
2203
2748
|
def _final_answer_observation_message(
|
|
2204
2749
|
*,
|
|
2205
2750
|
message: str,
|
|
@@ -2220,6 +2765,16 @@ def _final_answer_observation_message(
|
|
|
2220
2765
|
observation_json[:_FINAL_ANSWER_OBSERVATION_JSON_LIMIT] + "...[truncated]"
|
|
2221
2766
|
)
|
|
2222
2767
|
|
|
2768
|
+
document_guidance = ""
|
|
2769
|
+
if observation is not None and observation.get("primitive") == "document":
|
|
2770
|
+
document_guidance = (
|
|
2771
|
+
"\nDocument diff changes are the only approved edit claims. "
|
|
2772
|
+
"For document results, mention only result.status, text_summary, "
|
|
2773
|
+
"and diff.changes display_label/target_path/before_value/after_value. Do not add "
|
|
2774
|
+
"activity contents, achievements, plans, problems, improvements, "
|
|
2775
|
+
"or saved fields that are absent from diff.changes.\n"
|
|
2776
|
+
)
|
|
2777
|
+
|
|
2223
2778
|
return (
|
|
2224
2779
|
"[UMMAYA FINAL ANSWER OBSERVATION]\n"
|
|
2225
2780
|
f"{message}\n\n"
|
|
@@ -2227,6 +2782,7 @@ def _final_answer_observation_message(
|
|
|
2227
2782
|
f"{latest_user_utt}\n\n"
|
|
2228
2783
|
"Latest successful primitive tool_result JSON:\n"
|
|
2229
2784
|
f"{observation_json}\n\n"
|
|
2785
|
+
f"{document_guidance}"
|
|
2230
2786
|
"Use only the observed tool_result data above and the prior tool_result "
|
|
2231
2787
|
"messages. Do not call another tool. Do not invent names, addresses, "
|
|
2232
2788
|
"phone numbers, timestamps, weather values, receipt IDs, or source "
|
|
@@ -3072,82 +3628,47 @@ def _submit_requirement_for_query(user_query: str) -> dict[str, str] | None:
|
|
|
3072
3628
|
and _query_contains_any(user_query, ("신고", "신고서", "제출"))
|
|
3073
3629
|
)
|
|
3074
3630
|
if asks_submit and asks_hometax_tax_return:
|
|
3075
|
-
session_id = _extract_session_id(user_query, "HOMETAX-TAXRETURN-SESSION-001")
|
|
3076
|
-
params = {
|
|
3077
|
-
"tax_year": _extract_tax_year(user_query),
|
|
3078
|
-
"income_type": "종합소득",
|
|
3079
|
-
"total_income_krw": 42_000_000,
|
|
3080
|
-
"session_id": session_id,
|
|
3081
|
-
}
|
|
3082
3631
|
return {
|
|
3083
3632
|
"tool_id": "mock_submit_module_hometax_taxreturn",
|
|
3084
3633
|
"verify_tool_id": "mock_verify_module_modid",
|
|
3085
3634
|
"scope": "send:hometax.tax-return",
|
|
3086
3635
|
"pre_submit_lookup_tool_id": "mock_lookup_module_hometax_simplified",
|
|
3087
|
-
"params_json":
|
|
3636
|
+
"params_json": "{}",
|
|
3088
3637
|
}
|
|
3089
3638
|
|
|
3090
3639
|
if asks_submit and _query_contains_any(user_query, ("정부24", "주민등록등본", "등본", "민원")):
|
|
3091
|
-
session_id = _extract_session_id(user_query, "GOV24-MINWON-SESSION-001")
|
|
3092
|
-
params = {
|
|
3093
|
-
"minwon_type": "주민등록등본",
|
|
3094
|
-
"applicant_name": "홍길동" if "홍길동" in user_query else "MOCK_APPLICANT",
|
|
3095
|
-
"delivery_method": "online",
|
|
3096
|
-
"session_id": session_id,
|
|
3097
|
-
}
|
|
3098
3640
|
return {
|
|
3099
3641
|
"tool_id": "mock_submit_module_gov24_minwon",
|
|
3100
3642
|
"verify_tool_id": "mock_verify_module_simple_auth",
|
|
3101
3643
|
"scope": "send:gov24.minwon",
|
|
3102
|
-
"params_json":
|
|
3644
|
+
"params_json": "{}",
|
|
3103
3645
|
}
|
|
3104
3646
|
|
|
3105
3647
|
if asks_submit and _query_contains_any(
|
|
3106
3648
|
user_query,
|
|
3107
3649
|
("복지 급여", "복지신청", "한부모가족", "한부모", "아동양육비"),
|
|
3108
3650
|
):
|
|
3109
|
-
applicant_match = re.search(r"DI-[A-Z0-9-]+", user_query)
|
|
3110
|
-
household_match = re.search(r"(\d+)\s*명", user_query)
|
|
3111
|
-
params = {
|
|
3112
|
-
"applicant_id": applicant_match.group(0)
|
|
3113
|
-
if applicant_match
|
|
3114
|
-
else "DI-MOCK-WELFARE-APPLICANT",
|
|
3115
|
-
"benefit_code": "WLF00001068",
|
|
3116
|
-
"application_type": "new",
|
|
3117
|
-
"household_size": int(household_match.group(1)) if household_match else 1,
|
|
3118
|
-
}
|
|
3119
3651
|
return {
|
|
3120
3652
|
"tool_id": "mock_welfare_application_submit_v1",
|
|
3121
3653
|
"verify_tool_id": "mock_verify_mydata",
|
|
3122
3654
|
"scope": "send:mydata.welfare_application",
|
|
3123
|
-
"params_json":
|
|
3655
|
+
"params_json": "{}",
|
|
3124
3656
|
}
|
|
3125
3657
|
|
|
3126
3658
|
if asks_submit and _query_contains_any(user_query, ("과태료", "교통범칙금", "범칙금")):
|
|
3127
|
-
params = {
|
|
3128
|
-
"fine_reference": "MOCK-FINE-2026-001",
|
|
3129
|
-
"payment_method": "virtual_account",
|
|
3130
|
-
}
|
|
3131
3659
|
return {
|
|
3132
3660
|
"tool_id": "mock_traffic_fine_pay_v1",
|
|
3133
3661
|
"verify_tool_id": "mock_verify_ganpyeon_injeung",
|
|
3134
3662
|
"scope": "send:traffic.fine-pay",
|
|
3135
|
-
"params_json":
|
|
3663
|
+
"params_json": "{}",
|
|
3136
3664
|
}
|
|
3137
3665
|
|
|
3138
3666
|
if asks_submit and _query_contains_any(user_query, ("마이데이터", "공공마이데이터")):
|
|
3139
|
-
session_id = _extract_session_id(user_query, "MYDATA-ACTION-SESSION-001")
|
|
3140
|
-
params = {
|
|
3141
|
-
"action_type": "transfer_consent",
|
|
3142
|
-
"target_institution_code": "PUBLIC-MYDATA-MOCK",
|
|
3143
|
-
"applicant_di": "DI-MOCK-MYDATA-001",
|
|
3144
|
-
"session_id": session_id,
|
|
3145
|
-
}
|
|
3146
3667
|
return {
|
|
3147
3668
|
"tool_id": "mock_submit_module_public_mydata_action",
|
|
3148
3669
|
"verify_tool_id": "mock_verify_mydata",
|
|
3149
3670
|
"scope": "send:public_mydata.action",
|
|
3150
|
-
"params_json":
|
|
3671
|
+
"params_json": "{}",
|
|
3151
3672
|
}
|
|
3152
3673
|
|
|
3153
3674
|
return None
|
|
@@ -3176,17 +3697,18 @@ def _check_submit_terminated_without_submit(
|
|
|
3176
3697
|
tool_id=pre_submit_lookup_tool_id,
|
|
3177
3698
|
):
|
|
3178
3699
|
return None
|
|
3179
|
-
params_json = requirement["params_json"]
|
|
3180
3700
|
tool_id = requirement["tool_id"]
|
|
3701
|
+
scope = requirement["scope"]
|
|
3181
3702
|
return {
|
|
3182
3703
|
**requirement,
|
|
3183
3704
|
"message": (
|
|
3184
3705
|
"Send follow-up missing: the citizen asked to complete a write, "
|
|
3185
3706
|
"payment, consent, or filing flow and verification has already run, "
|
|
3186
3707
|
f"but {tool_id!r} has not succeeded. RECOVERY: in the next turn call "
|
|
3187
|
-
f"send
|
|
3188
|
-
"
|
|
3189
|
-
"
|
|
3708
|
+
f"send using tool_id {tool_id!r}, the verified {scope!r} delegation "
|
|
3709
|
+
"context, and params that satisfy the registered adapter schema. "
|
|
3710
|
+
"The backend will inject the cached DelegationContext. Do NOT invent "
|
|
3711
|
+
"mock fixture fields and do NOT end with guidance-only prose."
|
|
3190
3712
|
),
|
|
3191
3713
|
}
|
|
3192
3714
|
|
|
@@ -3279,17 +3801,10 @@ def _canonicalize_submit_tool_id(
|
|
|
3279
3801
|
def _apply_submit_canonical_params(
|
|
3280
3802
|
params: dict[str, object],
|
|
3281
3803
|
canonical: dict[str, object],
|
|
3282
|
-
|
|
3804
|
+
_tool_id: str,
|
|
3283
3805
|
) -> bool:
|
|
3284
|
-
"""Apply submit fixture defaults, overwriting Hometax mock guesses."""
|
|
3285
3806
|
changed = False
|
|
3286
|
-
overwrite = tool_id == "mock_submit_module_hometax_taxreturn"
|
|
3287
3807
|
for key, value in canonical.items():
|
|
3288
|
-
if overwrite:
|
|
3289
|
-
if params.get(key) != value:
|
|
3290
|
-
params[key] = value
|
|
3291
|
-
changed = True
|
|
3292
|
-
continue
|
|
3293
3808
|
if key not in params or params.get(key) in (None, ""):
|
|
3294
3809
|
params[key] = value
|
|
3295
3810
|
changed = True
|
|
@@ -3332,11 +3847,14 @@ def _normalize_submit_args_for_query(
|
|
|
3332
3847
|
|
|
3333
3848
|
def _strip_hometax_lookup_context_noise(params: dict[str, object]) -> bool:
|
|
3334
3849
|
"""Remove model-invented lookup fields from delegation_context."""
|
|
3850
|
+
changed = False
|
|
3851
|
+
if "query" in params:
|
|
3852
|
+
params.pop("query", None)
|
|
3853
|
+
changed = True
|
|
3335
3854
|
delegation_context = params.get("delegation_context")
|
|
3336
3855
|
if not isinstance(delegation_context, dict):
|
|
3337
|
-
return
|
|
3856
|
+
return changed
|
|
3338
3857
|
cleaned = dict(delegation_context)
|
|
3339
|
-
changed = False
|
|
3340
3858
|
for key in ("year", "resident_id_prefix"):
|
|
3341
3859
|
if key in cleaned:
|
|
3342
3860
|
cleaned.pop(key, None)
|
|
@@ -3382,6 +3900,58 @@ def _normalize_hometax_lookup_args_for_query(
|
|
|
3382
3900
|
return normalized
|
|
3383
3901
|
|
|
3384
3902
|
|
|
3903
|
+
def _gov24_certificate_type_from_query(user_query: str) -> str | None:
|
|
3904
|
+
if _query_contains_any(user_query, ("가족관계증명서", "가족 관계")):
|
|
3905
|
+
return "family_relations"
|
|
3906
|
+
if _query_contains_any(user_query, ("사업자등록증", "사업자 등록")):
|
|
3907
|
+
return "business_registration"
|
|
3908
|
+
if _query_contains_any(user_query, ("주민등록등본", "등본")):
|
|
3909
|
+
return "resident_registration"
|
|
3910
|
+
return None
|
|
3911
|
+
|
|
3912
|
+
|
|
3913
|
+
def _gov24_certificate_purpose_from_query(user_query: str, certificate_type: str) -> str:
|
|
3914
|
+
if certificate_type == "resident_registration" and _query_contains_any(
|
|
3915
|
+
user_query,
|
|
3916
|
+
("가능 여부", "준비물", "확인", "알려", "방법"),
|
|
3917
|
+
):
|
|
3918
|
+
return "주민등록등본 발급 가능 여부와 준비물 확인"
|
|
3919
|
+
if certificate_type == "family_relations":
|
|
3920
|
+
return "가족관계증명서 발급 정보 확인"
|
|
3921
|
+
if certificate_type == "business_registration":
|
|
3922
|
+
return "사업자등록증 발급 정보 확인"
|
|
3923
|
+
return "정부24 증명서 발급 정보 확인"
|
|
3924
|
+
|
|
3925
|
+
|
|
3926
|
+
def _normalize_gov24_certificate_lookup_args_for_query(
|
|
3927
|
+
args_obj: dict[str, object],
|
|
3928
|
+
user_query: str,
|
|
3929
|
+
) -> dict[str, object]:
|
|
3930
|
+
if args_obj.get("tool_id") != "mock_lookup_module_gov24_certificate":
|
|
3931
|
+
return args_obj
|
|
3932
|
+
if not _query_contains_any(
|
|
3933
|
+
user_query, ("정부24", "주민등록등본", "등본", "가족관계증명서", "사업자등록증")
|
|
3934
|
+
):
|
|
3935
|
+
return args_obj
|
|
3936
|
+
certificate_type = _gov24_certificate_type_from_query(user_query)
|
|
3937
|
+
if certificate_type is None:
|
|
3938
|
+
return args_obj
|
|
3939
|
+
raw_params = args_obj.get("params")
|
|
3940
|
+
params = dict(raw_params) if isinstance(raw_params, dict) else {}
|
|
3941
|
+
changed = not isinstance(raw_params, dict)
|
|
3942
|
+
if params.get("certificate_type") in (None, ""):
|
|
3943
|
+
params["certificate_type"] = certificate_type
|
|
3944
|
+
changed = True
|
|
3945
|
+
if params.get("purpose") in (None, ""):
|
|
3946
|
+
params["purpose"] = _gov24_certificate_purpose_from_query(user_query, certificate_type)
|
|
3947
|
+
changed = True
|
|
3948
|
+
if not changed:
|
|
3949
|
+
return args_obj
|
|
3950
|
+
normalized = dict(args_obj)
|
|
3951
|
+
normalized["params"] = params
|
|
3952
|
+
return normalized
|
|
3953
|
+
|
|
3954
|
+
|
|
3385
3955
|
def _canonicalize_lookup_tool_id_for_query(
|
|
3386
3956
|
args_obj: dict[str, object],
|
|
3387
3957
|
user_query: str,
|
|
@@ -3392,7 +3962,15 @@ def _canonicalize_lookup_tool_id_for_query(
|
|
|
3392
3962
|
return args_obj
|
|
3393
3963
|
sensitive_lookup = _sensitive_lookup_requirement_for_query(user_query)
|
|
3394
3964
|
if sensitive_lookup is None:
|
|
3395
|
-
|
|
3965
|
+
if not _query_contains_any(
|
|
3966
|
+
user_query,
|
|
3967
|
+
("홈택스", "연말정산", "간소화", "종합소득세", "소득세 신고", "세금 신고"),
|
|
3968
|
+
):
|
|
3969
|
+
return args_obj
|
|
3970
|
+
sensitive_lookup = {
|
|
3971
|
+
**_SENSITIVE_LOOKUP_AUTH_REQUIREMENTS["mock_lookup_module_hometax_simplified"],
|
|
3972
|
+
"tool_id": "mock_lookup_module_hometax_simplified",
|
|
3973
|
+
}
|
|
3396
3974
|
normalized = dict(args_obj)
|
|
3397
3975
|
normalized["tool_id"] = sensitive_lookup["tool_id"]
|
|
3398
3976
|
logger.info(
|
|
@@ -3471,6 +4049,7 @@ def _normalize_lookup_args_for_query(
|
|
|
3471
4049
|
return args_obj
|
|
3472
4050
|
args_obj = _canonicalize_lookup_tool_id_for_query(args_obj, user_query)
|
|
3473
4051
|
args_obj = _normalize_hometax_lookup_args_for_query(args_obj, user_query)
|
|
4052
|
+
args_obj = _normalize_gov24_certificate_lookup_args_for_query(args_obj, user_query)
|
|
3474
4053
|
args_obj = _normalize_lookup_result_count_args(
|
|
3475
4054
|
args_obj,
|
|
3476
4055
|
user_query,
|
|
@@ -3506,6 +4085,21 @@ def _normalize_lookup_args_for_query(
|
|
|
3506
4085
|
return normalized
|
|
3507
4086
|
|
|
3508
4087
|
|
|
4088
|
+
def _lookup_context_from_args(args_obj: dict[str, object]) -> str:
|
|
4089
|
+
chunks: list[str] = []
|
|
4090
|
+
for key in ("query", "request", "instruction", "purpose_ko"):
|
|
4091
|
+
value = args_obj.get(key)
|
|
4092
|
+
if isinstance(value, str) and value.strip():
|
|
4093
|
+
chunks.append(value.strip())
|
|
4094
|
+
raw_params = args_obj.get("params")
|
|
4095
|
+
if isinstance(raw_params, dict):
|
|
4096
|
+
for key in ("query", "request", "instruction", "purpose_ko"):
|
|
4097
|
+
value = raw_params.get(key)
|
|
4098
|
+
if isinstance(value, str) and value.strip():
|
|
4099
|
+
chunks.append(value.strip())
|
|
4100
|
+
return " ".join(chunks)
|
|
4101
|
+
|
|
4102
|
+
|
|
3509
4103
|
_KOREAN_COUNT_WORDS: Final[dict[str, int]] = {
|
|
3510
4104
|
"한": 1,
|
|
3511
4105
|
"두": 2,
|
|
@@ -4449,26 +5043,31 @@ def _emitted_tool_id(fname: str, args_obj: dict[str, object]) -> str | None:
|
|
|
4449
5043
|
return None
|
|
4450
5044
|
|
|
4451
5045
|
|
|
4452
|
-
def _direct_public_data_target_for_query(
|
|
5046
|
+
def _direct_public_data_target_for_query(
|
|
5047
|
+
user_query: str,
|
|
5048
|
+
) -> tuple[frozenset[str], str, str, str] | None:
|
|
4453
5049
|
"""Return target adapter family for public-data wording that should not use substitutes."""
|
|
4454
5050
|
if _KMA_ANALYSIS_MAP_USER_QUERY_RE.search(user_query):
|
|
4455
5051
|
return (
|
|
4456
5052
|
frozenset({_KMA_ANALYSIS_CHART_TOOL_ID}),
|
|
4457
5053
|
_KMA_ANALYSIS_CHART_TOOL_ID,
|
|
4458
|
-
"
|
|
4459
|
-
"
|
|
5054
|
+
"weather_chart",
|
|
5055
|
+
"use official KMA APIHub analyzed weather-chart evidence and do not "
|
|
5056
|
+
"substitute location, AirKorea, or ordinary weather evidence.",
|
|
4460
5057
|
)
|
|
4461
5058
|
if _PPS_BID_USER_QUERY_RE.search(user_query):
|
|
4462
5059
|
return (
|
|
4463
5060
|
frozenset({_PPS_BID_TOOL_ID}),
|
|
4464
5061
|
_PPS_BID_TOOL_ID,
|
|
4465
|
-
"
|
|
5062
|
+
"procurement_bid",
|
|
5063
|
+
"use PPS/NaraJangteo bid notice date fields.",
|
|
4466
5064
|
)
|
|
4467
5065
|
if _AIRKOREA_USER_QUERY_RE.search(user_query):
|
|
4468
5066
|
return (
|
|
4469
5067
|
frozenset({_AIRKOREA_TOOL_ID}),
|
|
4470
5068
|
_AIRKOREA_TOOL_ID,
|
|
4471
|
-
"
|
|
5069
|
+
"air_quality",
|
|
5070
|
+
"use AirKorea city/province air-quality evidence with sido_name such as '부산'.",
|
|
4472
5071
|
)
|
|
4473
5072
|
if _TAGO_BUS_USER_QUERY_RE.search(user_query):
|
|
4474
5073
|
preferred = (
|
|
@@ -4479,15 +5078,17 @@ def _direct_public_data_target_for_query(user_query: str) -> tuple[frozenset[str
|
|
|
4479
5078
|
return (
|
|
4480
5079
|
_TAGO_TOOL_IDS,
|
|
4481
5080
|
preferred,
|
|
4482
|
-
"
|
|
4483
|
-
"
|
|
5081
|
+
"bus_realtime",
|
|
5082
|
+
"use TAGO bus evidence; for a route number, start with route search, "
|
|
5083
|
+
"then route-station and arrival evidence.",
|
|
4484
5084
|
)
|
|
4485
5085
|
if _query_implies_current_weather_observation(user_query):
|
|
4486
5086
|
return (
|
|
4487
5087
|
_KMA_ORDINARY_WEATHER_TOOL_IDS | _KMA_LOCATION_TOOL_IDS,
|
|
4488
5088
|
"kakao_keyword_search",
|
|
4489
|
-
"
|
|
4490
|
-
"
|
|
5089
|
+
"current_weather",
|
|
5090
|
+
"use location resolution first when coordinates are missing, then "
|
|
5091
|
+
"KMA current observation evidence for rain/umbrella/current-weather values.",
|
|
4491
5092
|
)
|
|
4492
5093
|
return None
|
|
4493
5094
|
|
|
@@ -4501,15 +5102,15 @@ def _check_direct_public_data_tool_choice_prerequisite(
|
|
|
4501
5102
|
target = _direct_public_data_target_for_query(user_query)
|
|
4502
5103
|
if target is None:
|
|
4503
5104
|
return None
|
|
4504
|
-
allowed_tool_ids, preferred_tool_id, hint = target
|
|
5105
|
+
allowed_tool_ids, preferred_tool_id, route_label, hint = target
|
|
4505
5106
|
emitted_tool_id = _emitted_tool_id(fname, args_obj)
|
|
4506
5107
|
if emitted_tool_id is None or emitted_tool_id in allowed_tool_ids:
|
|
4507
5108
|
return None
|
|
4508
5109
|
return (
|
|
4509
5110
|
preferred_tool_id,
|
|
4510
|
-
"Public-data tool-choice mismatch:
|
|
4511
|
-
f"{
|
|
4512
|
-
f"RECOVERY: {hint}",
|
|
5111
|
+
"Public-data tool-choice mismatch: "
|
|
5112
|
+
f"target={route_label}. The latest citizen request needs that route; "
|
|
5113
|
+
f"the previous tool choice does not match. RECOVERY: {hint}",
|
|
4513
5114
|
)
|
|
4514
5115
|
|
|
4515
5116
|
|
|
@@ -4605,6 +5206,7 @@ _AVAILABLE_ADAPTER_FIND_LINE_RE: Final = re.compile(
|
|
|
4605
5206
|
r"^\s*-\s+[A-Za-z0-9_.:-]+\s+\(primitive=find\)",
|
|
4606
5207
|
re.MULTILINE,
|
|
4607
5208
|
)
|
|
5209
|
+
_AVAILABLE_ADAPTER_TOOL_ID_LINE_RE: Final = re.compile(r"^\s*-\s*tool_id:\s*[A-Za-z0-9_.:-]+\s*$")
|
|
4608
5210
|
_MEDICAL_COLLAPSE_RE: Final = re.compile(
|
|
4609
5211
|
r"(사람[이가은는 ]*쓰러|쓰러졌|쓰러져|의식[을 ]*(?:잃|없)|심정지|"
|
|
4610
5212
|
r"숨[을 ]*(?:안|못)|호흡[이가은는 ]*없|자동심장|심장충격|제세동|"
|
|
@@ -4639,7 +5241,21 @@ def _latest_available_adapters_block(llm_messages: list[Any]) -> str:
|
|
|
4639
5241
|
|
|
4640
5242
|
def _available_adapters_block_has_find_candidate(block: str) -> bool:
|
|
4641
5243
|
"""Return True when retrieval surfaced a non-locate follow-up adapter."""
|
|
4642
|
-
|
|
5244
|
+
if not block:
|
|
5245
|
+
return False
|
|
5246
|
+
if _AVAILABLE_ADAPTER_FIND_LINE_RE.search(block):
|
|
5247
|
+
return True
|
|
5248
|
+
in_projected_candidate = False
|
|
5249
|
+
for line in block.splitlines():
|
|
5250
|
+
stripped = line.strip()
|
|
5251
|
+
if _AVAILABLE_ADAPTER_TOOL_ID_LINE_RE.match(line):
|
|
5252
|
+
in_projected_candidate = True
|
|
5253
|
+
continue
|
|
5254
|
+
if stripped.startswith("- "):
|
|
5255
|
+
in_projected_candidate = False
|
|
5256
|
+
if in_projected_candidate and stripped == "primitive: find":
|
|
5257
|
+
return True
|
|
5258
|
+
return False
|
|
4643
5259
|
|
|
4644
5260
|
|
|
4645
5261
|
def _available_adapters_block_has_tool_id(block: str, tool_id: str) -> bool:
|
|
@@ -5180,6 +5796,7 @@ async def run( # noqa: C901
|
|
|
5180
5796
|
_fh.setFormatter(
|
|
5181
5797
|
logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s")
|
|
5182
5798
|
)
|
|
5799
|
+
_fh.addFilter(_BackendSecretRedactionFilter())
|
|
5183
5800
|
_root.addHandler(_fh)
|
|
5184
5801
|
_root.setLevel(min(_root.level or logging.INFO, logging.INFO))
|
|
5185
5802
|
logger.info(
|
|
@@ -5393,6 +6010,19 @@ async def run( # noqa: C901
|
|
|
5393
6010
|
_ensure_tool_registry() # populates both refs in one shot
|
|
5394
6011
|
return _tool_executor_ref[0]
|
|
5395
6012
|
|
|
6013
|
+
def _is_local_document_harness_root_call(
|
|
6014
|
+
fname: str,
|
|
6015
|
+
args_obj: dict[str, object],
|
|
6016
|
+
) -> bool:
|
|
6017
|
+
tool_id = str(args_obj.get("tool_id") or "")
|
|
6018
|
+
if not tool_id:
|
|
6019
|
+
return False
|
|
6020
|
+
try:
|
|
6021
|
+
tool = _ensure_tool_registry().find(tool_id)
|
|
6022
|
+
except Exception:
|
|
6023
|
+
return False
|
|
6024
|
+
return tool.primitive == fname and _is_local_document_harness_tool(tool)
|
|
6025
|
+
|
|
5396
6026
|
async def _ensure_llm_client() -> object:
|
|
5397
6027
|
if not _llm_client_ref:
|
|
5398
6028
|
from ummaya.llm.client import LLMClient # noqa: PLC0415
|
|
@@ -5557,54 +6187,50 @@ async def run( # noqa: C901
|
|
|
5557
6187
|
)
|
|
5558
6188
|
_root_primitive_tool_ids = _ROOT_PRIMITIVE_TOOL_IDS
|
|
5559
6189
|
|
|
5560
|
-
def
|
|
5561
|
-
"""Return concrete, non-core adapter tools for this citizen turn.
|
|
5562
|
-
|
|
5563
|
-
CC exposes concrete Tool objects to the model; UMMAYA keeps the same
|
|
5564
|
-
model-facing shape and uses BM25/dense retrieval only as a loading
|
|
5565
|
-
optimization so the tool list stays small.
|
|
5566
|
-
"""
|
|
6190
|
+
def _route_decision_for_turn(user_query: str) -> RouteDecision | None:
|
|
5567
6191
|
q = (user_query or "").strip()
|
|
5568
6192
|
if not q:
|
|
5569
|
-
return
|
|
6193
|
+
return None
|
|
5570
6194
|
registry = _ensure_tool_registry()
|
|
5571
|
-
selected: dict[str, Any] = {}
|
|
5572
|
-
for tool in registry.all_tools():
|
|
5573
|
-
if tool.id in _root_primitive_tool_ids:
|
|
5574
|
-
continue
|
|
5575
|
-
if tool.id in q:
|
|
5576
|
-
selected[tool.id] = tool
|
|
5577
6195
|
try:
|
|
5578
|
-
from ummaya.tools.
|
|
6196
|
+
from ummaya.tools.routing import RouteDecisionService # noqa: PLC0415
|
|
5579
6197
|
|
|
5580
6198
|
raw_top_k = max(_AVAILABLE_ADAPTERS_TOP_K * 3, _AVAILABLE_ADAPTERS_TOP_K)
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
bm25_index=registry.bm25_index,
|
|
5584
|
-
registry=registry,
|
|
6199
|
+
return RouteDecisionService(registry).select_adapters(
|
|
6200
|
+
q,
|
|
5585
6201
|
top_k=min(raw_top_k, 20),
|
|
6202
|
+
max_selected=_AVAILABLE_ADAPTERS_TOP_K,
|
|
5586
6203
|
)
|
|
5587
6204
|
except Exception:
|
|
5588
|
-
logger.exception("
|
|
5589
|
-
|
|
5590
|
-
for candidate in candidates:
|
|
5591
|
-
try:
|
|
5592
|
-
tool = registry.find(candidate.tool_id)
|
|
5593
|
-
except Exception:
|
|
5594
|
-
logger.debug(
|
|
5595
|
-
"Skipping unavailable adapter candidate %s",
|
|
5596
|
-
candidate.tool_id,
|
|
5597
|
-
exc_info=True,
|
|
5598
|
-
)
|
|
5599
|
-
continue
|
|
5600
|
-
if tool.id in _root_primitive_tool_ids:
|
|
5601
|
-
continue
|
|
5602
|
-
selected.setdefault(tool.id, tool)
|
|
5603
|
-
if len(selected) >= _AVAILABLE_ADAPTERS_TOP_K:
|
|
5604
|
-
break
|
|
5605
|
-
return list(selected.values())[:_AVAILABLE_ADAPTERS_TOP_K]
|
|
6205
|
+
logger.exception("route decision failed for '%s'", q[:80])
|
|
6206
|
+
return None
|
|
5606
6207
|
|
|
5607
|
-
def
|
|
6208
|
+
def _select_concrete_adapter_tools_for_turn(
|
|
6209
|
+
user_query: str, route_decision: RouteDecision | None = None
|
|
6210
|
+
) -> list[Any]:
|
|
6211
|
+
q = (user_query or "").strip()
|
|
6212
|
+
if not q:
|
|
6213
|
+
return []
|
|
6214
|
+
registry = _ensure_tool_registry()
|
|
6215
|
+
decision = route_decision or _route_decision_for_turn(q)
|
|
6216
|
+
if decision is None:
|
|
6217
|
+
return []
|
|
6218
|
+
from ummaya.tools.routing import selected_concrete_adapter_tools # noqa: PLC0415
|
|
6219
|
+
|
|
6220
|
+
return list(
|
|
6221
|
+
selected_concrete_adapter_tools(
|
|
6222
|
+
decision,
|
|
6223
|
+
registry,
|
|
6224
|
+
exclude_tool_ids=_root_primitive_tool_ids,
|
|
6225
|
+
max_tools=_AVAILABLE_ADAPTERS_TOP_K,
|
|
6226
|
+
)
|
|
6227
|
+
)
|
|
6228
|
+
|
|
6229
|
+
def _build_available_adapters_suffix(
|
|
6230
|
+
user_query: str,
|
|
6231
|
+
route_decision: RouteDecision | None = None,
|
|
6232
|
+
visible_tool_ids: Iterable[str] | None = None,
|
|
6233
|
+
) -> str: # noqa: C901
|
|
5608
6234
|
"""Run BM25 against the live registry and emit the citizen-turn
|
|
5609
6235
|
``<available_adapters>`` XML block for the dynamic system-prompt
|
|
5610
6236
|
suffix.
|
|
@@ -5618,281 +6244,32 @@ async def run( # noqa: C901
|
|
|
5618
6244
|
q = (user_query or "").strip()
|
|
5619
6245
|
if not q:
|
|
5620
6246
|
return ""
|
|
6247
|
+
route_decision = route_decision or _route_decision_for_turn(q)
|
|
6248
|
+
if route_decision is None:
|
|
6249
|
+
return ""
|
|
5621
6250
|
try:
|
|
5622
|
-
from ummaya.tools.
|
|
6251
|
+
from ummaya.tools.routing import build_available_adapters_projection # noqa: PLC0415
|
|
5623
6252
|
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
|
|
6253
|
+
visible_tool_ids_tuple = None if visible_tool_ids is None else tuple(visible_tool_ids)
|
|
6254
|
+
projection_level = (
|
|
6255
|
+
route_decision.schema_projection_level
|
|
6256
|
+
if route_decision.selected_tools or not visible_tool_ids_tuple
|
|
6257
|
+
else "summary"
|
|
6258
|
+
)
|
|
6259
|
+
projection = build_available_adapters_projection(
|
|
6260
|
+
route_decision,
|
|
6261
|
+
_ensure_tool_registry(),
|
|
5627
6262
|
query=q,
|
|
5628
|
-
|
|
5629
|
-
|
|
5630
|
-
|
|
6263
|
+
projection_level=projection_level,
|
|
6264
|
+
max_visible=_AVAILABLE_ADAPTERS_TOP_K
|
|
6265
|
+
if visible_tool_ids_tuple is None
|
|
6266
|
+
else len(visible_tool_ids_tuple),
|
|
6267
|
+
visible_tool_ids=visible_tool_ids_tuple,
|
|
5631
6268
|
)
|
|
6269
|
+
return projection.content or ""
|
|
5632
6270
|
except Exception:
|
|
5633
|
-
logger.exception("
|
|
5634
|
-
return ""
|
|
5635
|
-
filtered_candidates = []
|
|
5636
|
-
for candidate in candidates:
|
|
5637
|
-
try:
|
|
5638
|
-
tool = registry.find(candidate.tool_id)
|
|
5639
|
-
except Exception:
|
|
5640
|
-
logger.debug(
|
|
5641
|
-
"Skipping unavailable adapter candidate %s",
|
|
5642
|
-
candidate.tool_id,
|
|
5643
|
-
exc_info=True,
|
|
5644
|
-
)
|
|
5645
|
-
continue
|
|
5646
|
-
if tool.id in _root_primitive_tool_ids:
|
|
5647
|
-
continue
|
|
5648
|
-
filtered_candidates.append(candidate)
|
|
5649
|
-
if len(filtered_candidates) >= _AVAILABLE_ADAPTERS_TOP_K:
|
|
5650
|
-
break
|
|
5651
|
-
candidates = filtered_candidates
|
|
5652
|
-
if not candidates:
|
|
6271
|
+
logger.exception("route decision projection failed for '%s'", q[:80])
|
|
5653
6272
|
return ""
|
|
5654
|
-
candidate_ids = tuple(candidate.tool_id for candidate in candidates)
|
|
5655
|
-
first_candidate_id = candidate_ids[0]
|
|
5656
|
-
has_amos_candidate = "kma_apihub_url_air_amos_minute" in candidate_ids
|
|
5657
|
-
has_metar_candidate = "kma_apihub_url_air_metar_decoded" in candidate_ids
|
|
5658
|
-
has_analysis_candidate = any(
|
|
5659
|
-
candidate_id
|
|
5660
|
-
in {
|
|
5661
|
-
"kma_apihub_url_high_resolution_grid_point",
|
|
5662
|
-
"kma_apihub_url_aws_objective_analysis_grid",
|
|
5663
|
-
"kma_apihub_url_analysis_weather_chart_image",
|
|
5664
|
-
}
|
|
5665
|
-
for candidate_id in candidate_ids
|
|
5666
|
-
)
|
|
5667
|
-
is_gimpo_runway_query = bool(
|
|
5668
|
-
re.search(r"(김포공항|Gimpo|RKSS)", q, re.IGNORECASE)
|
|
5669
|
-
and re.search(
|
|
5670
|
-
r"(AMOS|활주로|RVR|runway|시정|visibility|공항기상관측|매분)",
|
|
5671
|
-
q,
|
|
5672
|
-
re.IGNORECASE,
|
|
5673
|
-
)
|
|
5674
|
-
)
|
|
5675
|
-
# Build a compact, LLM-readable block.
|
|
5676
|
-
#
|
|
5677
|
-
# Spec 2521 (2026-05-02) — emit per-field schema signatures so the
|
|
5678
|
-
# LLM can fill ``params`` against each adapter's actual REST shape.
|
|
5679
|
-
# The previous suffix only carried ``search_hint`` and assumed the
|
|
5680
|
-
# LLM could "infer params from search_hint" — K-EXAONE on FriendliAI
|
|
5681
|
-
# consistently invented ``{"location": "...", "date": "..."}`` style
|
|
5682
|
-
# payloads which fail every adapter's pydantic validation
|
|
5683
|
-
# (``Invalid parameters for tool``). Rendering each field with its
|
|
5684
|
-
# type + required flag + truncated description gives K-EXAONE
|
|
5685
|
-
# enough signal to call e.g. ``{"lat": 37.5, "lon": 129.0,
|
|
5686
|
-
# "base_date": "20260502", "base_time": "0500"}`` correctly.
|
|
5687
|
-
lines: list[str] = [
|
|
5688
|
-
f'<available_adapters query="{q[:120]}">',
|
|
5689
|
-
f"백엔드 BM25 후보 (top {len(candidates)}, 점수 내림차순):",
|
|
5690
|
-
"",
|
|
5691
|
-
]
|
|
5692
|
-
for c in candidates:
|
|
5693
|
-
hint = (c.search_hint or "").strip()
|
|
5694
|
-
if len(hint) > 90:
|
|
5695
|
-
hint = hint[:87] + "..."
|
|
5696
|
-
primitive = c.primitive or "find"
|
|
5697
|
-
lines.append(
|
|
5698
|
-
f"- {c.tool_id} (primitive={primitive}) [{c.score:.2f}] — {hint or '(설명 없음)'}"
|
|
5699
|
-
)
|
|
5700
|
-
lines.append(f" 호출: {c.tool_id}({{...schema fields...}})")
|
|
5701
|
-
# Render the adapter's llm_description (usage prose, ORDERING RULE,
|
|
5702
|
-
# prerequisites, worked examples) so the LLM sees the complete
|
|
5703
|
-
# "먼저 locate 호출" ordering rule.
|
|
5704
|
-
# Bug: without this, the per-field description for nx is truncated
|
|
5705
|
-
# and K-EXAONE skips locate, producing invalid_params.
|
|
5706
|
-
if c.llm_description:
|
|
5707
|
-
desc_text = c.llm_description.strip().replace("\n", " ")
|
|
5708
|
-
# Emit enough text for adapter-specific negative routing and
|
|
5709
|
-
# output-use rules. KMA METAR/AMOS descriptions carry critical
|
|
5710
|
-
# "Gimhae is not AMOS" and "safe_weather only" instructions
|
|
5711
|
-
# after the purpose sentence; truncating them makes the TUI
|
|
5712
|
-
# path claim no METAR tool exists.
|
|
5713
|
-
if len(desc_text) > 900:
|
|
5714
|
-
desc_text = desc_text[:897] + "..."
|
|
5715
|
-
lines.append(f" 설명: {desc_text}")
|
|
5716
|
-
# Render input schema signature so the LLM sees exact field
|
|
5717
|
-
# names + types + required flags + (truncated) descriptions.
|
|
5718
|
-
# Field desc limit raised 80→120 so nx/ny examples fit untruncated.
|
|
5719
|
-
schema = c.input_schema_json or {}
|
|
5720
|
-
properties = schema.get("properties") if isinstance(schema, dict) else None
|
|
5721
|
-
required: set[str] = set()
|
|
5722
|
-
raw_required = schema.get("required") if isinstance(schema, dict) else None
|
|
5723
|
-
if isinstance(raw_required, list):
|
|
5724
|
-
required = {str(item) for item in raw_required if isinstance(item, str)}
|
|
5725
|
-
# Spec 2522 T010 — ORDERING directive removed.
|
|
5726
|
-
# The Spec 2521 ORDERING block ("nx/ny 는 KMA 격자 좌표 — 반드시
|
|
5727
|
-
# locate 을 먼저 호출") forced a cross-domain chain that
|
|
5728
|
-
# contradicts both the user directive ("chain X / UMMAYA does not
|
|
5729
|
-
# force cross-domain chain") and v4 description 5-section
|
|
5730
|
-
# self_contained_decl ("이 도구 단독 호출로 완결. locate 등
|
|
5731
|
-
# cross-domain chain 불필요"). With both signals present K-EXAONE
|
|
5732
|
-
# ignored both and hallucinated nx/ny → Spec 2521 regression.
|
|
5733
|
-
# Each adapter's description (섹션 4 domain_quirk + 섹션 5
|
|
5734
|
-
# self_contained_decl + 섹션 3 short_reference 17 광역시도 표) is now
|
|
5735
|
-
# self-sufficient. The model decides chain vs single-tool autonomously.
|
|
5736
|
-
# Reference: research-stdio-ordering.md, frames-busan-weather/ T042 evidence.
|
|
5737
|
-
# Spec 2522 T047 fix — resolve $ref to $defs and inline enum values.
|
|
5738
|
-
# KOROAD KoroadAccidentSearchInput.search_year_cd uses
|
|
5739
|
-
# `$ref: #/$defs/SearchYearCd` (20 values). The previous renderer
|
|
5740
|
-
# only inlined `properties.<f>.enum` and gave up on $ref, leaving
|
|
5741
|
-
# K-EXAONE to guess plain '2024' (invalid). Spec 2522 frames-gangnam-
|
|
5742
|
-
# accident-fix2 evidence: invalid_params persisted after T042 fix.
|
|
5743
|
-
# Fix: resolve $ref against schema['$defs'] + raise threshold 8→25.
|
|
5744
|
-
defs_raw = schema.get("$defs") if isinstance(schema, dict) else None
|
|
5745
|
-
defs: dict[str, Any] | None = defs_raw if isinstance(defs_raw, dict) else None
|
|
5746
|
-
|
|
5747
|
-
def _resolve_enum(
|
|
5748
|
-
meta: dict[str, Any], defs: dict[str, Any] | None
|
|
5749
|
-
) -> list[Any] | None:
|
|
5750
|
-
# direct enum
|
|
5751
|
-
e = meta.get("enum")
|
|
5752
|
-
if isinstance(e, list):
|
|
5753
|
-
return e
|
|
5754
|
-
# $ref → $defs/<name>
|
|
5755
|
-
ref = meta.get("$ref")
|
|
5756
|
-
if isinstance(ref, str) and ref.startswith("#/$defs/") and isinstance(defs, dict):
|
|
5757
|
-
name = ref.removeprefix("#/$defs/")
|
|
5758
|
-
target = defs.get(name)
|
|
5759
|
-
if isinstance(target, dict):
|
|
5760
|
-
target_enum = target.get("enum")
|
|
5761
|
-
if isinstance(target_enum, list):
|
|
5762
|
-
return target_enum
|
|
5763
|
-
return None
|
|
5764
|
-
|
|
5765
|
-
def _resolve_enum_with_names(
|
|
5766
|
-
meta: dict[str, Any], defs: dict[str, Any] | None
|
|
5767
|
-
) -> list[tuple[Any, str]] | None:
|
|
5768
|
-
"""Spec 2522 — agency 자체 코드체계 (KOROAD GugunCode SEOUL_GANGNAM=680
|
|
5769
|
-
등) 의 IntEnum name 을 의미 매핑으로 노출. pydantic JSON schema 의
|
|
5770
|
-
$defs 안 IntEnum 의 'enum' (값) + 'x-enum-varnames' (name) 또는
|
|
5771
|
-
'description' (docstring) 을 묶어서 LLM 에 보여줌.
|
|
5772
|
-
"""
|
|
5773
|
-
ref = meta.get("$ref")
|
|
5774
|
-
if not (isinstance(ref, str) and ref.startswith("#/$defs/")):
|
|
5775
|
-
return None
|
|
5776
|
-
if not isinstance(defs, dict):
|
|
5777
|
-
return None
|
|
5778
|
-
name = ref.removeprefix("#/$defs/")
|
|
5779
|
-
target = defs.get(name)
|
|
5780
|
-
if not isinstance(target, dict):
|
|
5781
|
-
return None
|
|
5782
|
-
values = target.get("enum")
|
|
5783
|
-
if not isinstance(values, list):
|
|
5784
|
-
return None
|
|
5785
|
-
# IntEnum name 추출 — pydantic v2 가 'x-enum-varnames' 또는
|
|
5786
|
-
# 'enumNames' 로 export 하지 않음. 대신 module-level dict 조회.
|
|
5787
|
-
varnames = target.get("x-enum-varnames")
|
|
5788
|
-
if isinstance(varnames, list) and len(varnames) == len(values):
|
|
5789
|
-
return list(zip(values, varnames, strict=False))
|
|
5790
|
-
return None
|
|
5791
|
-
|
|
5792
|
-
if isinstance(properties, dict) and properties:
|
|
5793
|
-
for fname, fmeta in properties.items():
|
|
5794
|
-
if not isinstance(fmeta, dict):
|
|
5795
|
-
continue
|
|
5796
|
-
ftype = fmeta.get("type") or fmeta.get("anyOf") or "any"
|
|
5797
|
-
if isinstance(ftype, list):
|
|
5798
|
-
ftype = "|".join(str(t) for t in ftype)
|
|
5799
|
-
fdesc = str(fmeta.get("description", "")).strip().replace("\n", " ")
|
|
5800
|
-
# Spec 2522 — agency 자체 코드체계 (KOROAD 68 시군구 매핑 ≈ 1600
|
|
5801
|
-
# chars + 기존 description ≈ 600 chars = ~2200 chars / KMA 156
|
|
5802
|
-
# station 등) 인라인 허용. 일반 도구는 100자 미만이라 영향 X.
|
|
5803
|
-
if len(fdesc) > 5000:
|
|
5804
|
-
fdesc = fdesc[:4997] + "..."
|
|
5805
|
-
pat = fmeta.get("pattern")
|
|
5806
|
-
pat_part = f" pattern={pat!r}" if isinstance(pat, str) else ""
|
|
5807
|
-
enum = _resolve_enum(fmeta, defs)
|
|
5808
|
-
# Spec 2522 T047 — threshold 25→200 — KOROAD GugunCode (115) /
|
|
5809
|
-
# SearchYearCd (20) / SidoCode (17) 등 모두 노출. 의미 매핑은
|
|
5810
|
-
# field description 에 따로 인라인 (Pydantic IntEnum 의 name
|
|
5811
|
-
# 은 JSON schema 표준 export 안 됨).
|
|
5812
|
-
if isinstance(enum, list) and len(enum) <= 200:
|
|
5813
|
-
enum_part = f" enum={enum}"
|
|
5814
|
-
else:
|
|
5815
|
-
enum_part = ""
|
|
5816
|
-
flag = "필수" if fname in required else "선택"
|
|
5817
|
-
lines.append(
|
|
5818
|
-
f" · {fname} ({ftype}, {flag}{pat_part}{enum_part})"
|
|
5819
|
-
+ (f" — {fdesc}" if fdesc else "")
|
|
5820
|
-
)
|
|
5821
|
-
lines.append("")
|
|
5822
|
-
lines.append(
|
|
5823
|
-
"규칙: 위 목록의 tool_id는 concrete adapter id입니다. model-facing "
|
|
5824
|
-
"함수명도 tools[]에 로드된 concrete tool_id입니다. concrete adapter "
|
|
5825
|
-
"function은 schema 필드만 받으므로 tool_id/params envelope를 그 안에 "
|
|
5826
|
-
"넣지 마세요. concrete function이 로드되지 않고 root primitive만 "
|
|
5827
|
-
'있을 때만 legacy envelope 예: find({"tool_id":"...", "params":{...}}) '
|
|
5828
|
-
"형식을 사용합니다. 동일 tool_id 를 한 turn 안에서 반복 호출하지 "
|
|
5829
|
-
"마세요. 위 목록에 요청과 일치하는 adapter가 있으면 도구가 없다고 "
|
|
5830
|
-
"답하지 마세요."
|
|
5831
|
-
)
|
|
5832
|
-
if has_analysis_candidate:
|
|
5833
|
-
lines.append(
|
|
5834
|
-
"분석자료 특수 규칙: 위 후보에 고해상도 격자자료, AWS 객관분석, "
|
|
5835
|
-
"분석일기도 이미지가 있으면 기상청이 이미 분석한 자료 도구가 있는 "
|
|
5836
|
-
"것입니다. 공항 관측값/METAR/AMOS/일반 예보가 아니라 시민이 말한 "
|
|
5837
|
-
"분석자료 계열 후보를 호출하세요. 지도/일기도/비구름/바람 흐름 "
|
|
5838
|
-
"질의는 kma_apihub_url_analysis_weather_chart_image 를 우선 호출하고, "
|
|
5839
|
-
"특정 지점 주변 값은 locate 뒤 "
|
|
5840
|
-
"kma_apihub_url_high_resolution_grid_point 또는 "
|
|
5841
|
-
"kma_apihub_url_aws_objective_analysis_grid 를 호출하세요. 공항/랜드마크 "
|
|
5842
|
-
"주변 좌표는 kakao_keyword_search 를 kakao_address_search 보다 먼저 "
|
|
5843
|
-
"사용하세요. locate 가 실패하면 다른 후보 위치 도구를 시도하고, 도구 "
|
|
5844
|
-
"결과 없이 좌표를 추정하지 마세요. APIHub 승인 대기나 upstream 오류가 "
|
|
5845
|
-
"나면 그 실패를 그대로 설명하고, 도구 결과 없이 지도 기반 내용을 "
|
|
5846
|
-
"추정하지 마세요."
|
|
5847
|
-
)
|
|
5848
|
-
if has_amos_candidate and (
|
|
5849
|
-
is_gimpo_runway_query or first_candidate_id == "kma_apihub_url_air_amos_minute"
|
|
5850
|
-
):
|
|
5851
|
-
lines.append(
|
|
5852
|
-
"AMOS 특수 규칙: kma_apihub_url_air_amos_minute 가 김포공항 "
|
|
5853
|
-
"활주로/시정/RVR/매분 관측 후보이면 AMOS 공항기상관측 도구가 "
|
|
5854
|
-
"있는 것입니다. 김포공항은 stn=110 을 사용하세요. 이 후보는 "
|
|
5855
|
-
"좌표를 요구하지 않으므로 locate/kma_current_observation 을 먼저 "
|
|
5856
|
-
'호출하지 말고 즉시 kma_apihub_url_air_amos_minute({"stn":"110",'
|
|
5857
|
-
'"help":1}) 를 호출하세요. METAR 는 '
|
|
5858
|
-
"보조 확인이 필요할 때만 추가로 사용하세요."
|
|
5859
|
-
)
|
|
5860
|
-
if has_metar_candidate and not (has_amos_candidate and is_gimpo_runway_query):
|
|
5861
|
-
lines.append(
|
|
5862
|
-
"METAR 특수 규칙: kma_apihub_url_air_metar_decoded 가 후보에 있으면 "
|
|
5863
|
-
"공항 METAR 해독자료 조회 도구가 있는 것입니다. 김해공항/RKPK는 "
|
|
5864
|
-
"decoded_records 의 station 153 Gimhae Airport / RKPK record를 "
|
|
5865
|
-
"사용하고, 날씨 값은 decoded_records[].safe_weather 만 사용하세요. "
|
|
5866
|
-
"raw_fields/raw_report에서 별도 값을 만들지 마세요. 이 후보는 좌표를 "
|
|
5867
|
-
"요구하지 않으므로 locate/kma_current_observation 을 먼저 호출하지 "
|
|
5868
|
-
'말고 즉시 kma_apihub_url_air_metar_decoded({"org":"K","help":1}) '
|
|
5869
|
-
"를 호출하세요."
|
|
5870
|
-
)
|
|
5871
|
-
listed_primitives = {str(candidate.primitive or "find") for candidate in candidates}
|
|
5872
|
-
if listed_primitives == {"find"}:
|
|
5873
|
-
lines.append(
|
|
5874
|
-
"공개자료 조회 규칙: 위 후보가 모두 primitive=find 이면 시민이 "
|
|
5875
|
-
"인증/본인확인/동의/신청/제출/납부/신고를 명시하지 않은 한 "
|
|
5876
|
-
"check/send 계열 adapter를 호출하지 마세요. 성공한 find 결과가 있으면 "
|
|
5877
|
-
"다음 turn 은 최종 답변입니다."
|
|
5878
|
-
)
|
|
5879
|
-
lines.append(
|
|
5880
|
-
"호출 전 검증: 시민 발화의 명시 조건(개수, 반경/거리, 날짜/시간, 종류, "
|
|
5881
|
-
"카테고리, 진료과/분야, 키워드, 행정구역 등)이 아래 schema 의 선택 "
|
|
5882
|
-
"필드와 대응하면 그 필드를 반드시 params 에 포함하세요. 더 좁은 요청을 "
|
|
5883
|
-
"넓은 무필터 조회로 실행하지 마세요."
|
|
5884
|
-
)
|
|
5885
|
-
lines.append(
|
|
5886
|
-
'params 는 위에 표시된 정확한 필드명만 사용하세요 — 일반적인 "location"/'
|
|
5887
|
-
'"date" 같은 추측 키는 모든 어댑터에서 invalid_params 로 거부됩니다.'
|
|
5888
|
-
)
|
|
5889
|
-
lines.append(
|
|
5890
|
-
"BM25 도구 발견은 백엔드 internal 기능입니다. 모델은 검색 함수를 호출하지 "
|
|
5891
|
-
"않고, backend가 tools[]에 실어준 concrete adapter function을 우선 "
|
|
5892
|
-
"호출합니다."
|
|
5893
|
-
)
|
|
5894
|
-
lines.append("</available_adapters>")
|
|
5895
|
-
return "\n".join(lines)
|
|
5896
6273
|
|
|
5897
6274
|
# Spec 1978 T053 — eager-import the Mock adapter tree so every adapter
|
|
5898
6275
|
# self-registers with its primitive dispatcher before the first chat
|
|
@@ -6342,9 +6719,28 @@ async def run( # noqa: C901
|
|
|
6342
6719
|
span.set_attribute("ummaya.tool.dispatched", fname)
|
|
6343
6720
|
span.set_attribute("ummaya.session.id", session_id)
|
|
6344
6721
|
|
|
6722
|
+
if fname == "document" and "tool_id" not in args_obj:
|
|
6723
|
+
args_obj = {
|
|
6724
|
+
"tool_id": "document",
|
|
6725
|
+
"params": dict(args_obj),
|
|
6726
|
+
}
|
|
6727
|
+
normalized_document_args = _normalize_document_root_call_for_user_intent(
|
|
6728
|
+
fname,
|
|
6729
|
+
args_obj,
|
|
6730
|
+
_session_latest_user_utterances.get(session_id, ""),
|
|
6731
|
+
)
|
|
6732
|
+
if normalized_document_args is not args_obj:
|
|
6733
|
+
span.set_attribute("ummaya.document.intent_normalized", True)
|
|
6734
|
+
logger.warning(
|
|
6735
|
+
"_dispatch_primitive: normalized document root call to match latest "
|
|
6736
|
+
"citizen write/save intent."
|
|
6737
|
+
)
|
|
6738
|
+
args_obj = normalized_document_args
|
|
6739
|
+
|
|
6740
|
+
local_document_harness_call = _is_local_document_harness_root_call(fname, args_obj)
|
|
6345
6741
|
invalid_gated_tool_id = (
|
|
6346
6742
|
_invalid_gated_primitive_tool_id_result(fname, args_obj)
|
|
6347
|
-
if fname in _PERMISSION_GATED_PRIMITIVES
|
|
6743
|
+
if fname in _PERMISSION_GATED_PRIMITIVES and not local_document_harness_call
|
|
6348
6744
|
else None
|
|
6349
6745
|
)
|
|
6350
6746
|
if invalid_gated_tool_id is not None:
|
|
@@ -6375,13 +6771,17 @@ async def run( # noqa: C901
|
|
|
6375
6771
|
return
|
|
6376
6772
|
|
|
6377
6773
|
# ----- Permission gate (T043-T049) -----
|
|
6378
|
-
|
|
6379
|
-
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
|
|
6384
|
-
|
|
6774
|
+
if not local_document_harness_call:
|
|
6775
|
+
allowed = await _check_permission_gate(
|
|
6776
|
+
call_id, fname, args_obj, session_id, correlation_id
|
|
6777
|
+
)
|
|
6778
|
+
if not allowed:
|
|
6779
|
+
# Gate already resolved the Future with an error envelope.
|
|
6780
|
+
span.set_attribute("ummaya.permission.decision", "deny")
|
|
6781
|
+
return
|
|
6782
|
+
else:
|
|
6783
|
+
span.set_attribute("ummaya.permission.mode", "local_document_harness")
|
|
6784
|
+
span.set_attribute("ummaya.permission.decision", "allow_once")
|
|
6385
6785
|
|
|
6386
6786
|
result_payload: dict[str, object] = {}
|
|
6387
6787
|
dispatch_error: str | None = None
|
|
@@ -6403,7 +6803,43 @@ async def run( # noqa: C901
|
|
|
6403
6803
|
_outbound_trace_token = start_outbound_capture()
|
|
6404
6804
|
|
|
6405
6805
|
try:
|
|
6406
|
-
|
|
6806
|
+
document_harness_dispatched = False
|
|
6807
|
+
document_tool_id = str(args_obj.get("tool_id") or "")
|
|
6808
|
+
if document_tool_id:
|
|
6809
|
+
registry = _ensure_tool_registry()
|
|
6810
|
+
try:
|
|
6811
|
+
document_tool = registry.find(document_tool_id)
|
|
6812
|
+
except Exception:
|
|
6813
|
+
document_tool = None
|
|
6814
|
+
if document_tool is not None and _is_local_document_harness_tool(document_tool):
|
|
6815
|
+
document_harness_dispatched = True
|
|
6816
|
+
if document_tool.primitive != fname:
|
|
6817
|
+
dispatch_error = (
|
|
6818
|
+
f"Adapter {document_tool_id!r} is "
|
|
6819
|
+
f"primitive={document_tool.primitive!r}, "
|
|
6820
|
+
f"but was called through {fname}."
|
|
6821
|
+
)
|
|
6822
|
+
else:
|
|
6823
|
+
executor = _ensure_tool_executor()
|
|
6824
|
+
document_params = cast(
|
|
6825
|
+
"dict[str, object]",
|
|
6826
|
+
args_obj.get("params") or {},
|
|
6827
|
+
)
|
|
6828
|
+
raw = await executor.invoke_raw(
|
|
6829
|
+
tool_id=document_tool_id,
|
|
6830
|
+
params=document_params,
|
|
6831
|
+
request_id=str(uuid.uuid4()),
|
|
6832
|
+
session_identity=session_id,
|
|
6833
|
+
)
|
|
6834
|
+
result_payload = {
|
|
6835
|
+
"kind": fname,
|
|
6836
|
+
"result": _serialize_primitive_result(raw),
|
|
6837
|
+
}
|
|
6838
|
+
|
|
6839
|
+
if document_harness_dispatched:
|
|
6840
|
+
pass
|
|
6841
|
+
|
|
6842
|
+
elif fname == "check":
|
|
6407
6843
|
from ummaya.primitives.verify import ( # noqa: PLC0415
|
|
6408
6844
|
verify,
|
|
6409
6845
|
)
|
|
@@ -6476,6 +6912,14 @@ async def run( # noqa: C901
|
|
|
6476
6912
|
LookupFetchInput,
|
|
6477
6913
|
)
|
|
6478
6914
|
|
|
6915
|
+
lookup_context = _session_latest_user_utterances.get(
|
|
6916
|
+
session_id, ""
|
|
6917
|
+
) or _lookup_context_from_args(args_obj)
|
|
6918
|
+
args_obj = _normalize_lookup_args_for_query(
|
|
6919
|
+
fname,
|
|
6920
|
+
args_obj,
|
|
6921
|
+
lookup_context,
|
|
6922
|
+
)
|
|
6479
6923
|
requested_mode = args_obj.get("mode")
|
|
6480
6924
|
if requested_mode is not None and str(requested_mode) != "fetch":
|
|
6481
6925
|
logger.warning(
|
|
@@ -6516,17 +6960,31 @@ async def run( # noqa: C901
|
|
|
6516
6960
|
lookup_params,
|
|
6517
6961
|
auth_context,
|
|
6518
6962
|
)
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
|
|
6525
|
-
|
|
6526
|
-
|
|
6527
|
-
|
|
6528
|
-
|
|
6529
|
-
|
|
6963
|
+
lookup_tool_id = str(args_obj.get("tool_id") or "").strip()
|
|
6964
|
+
if not lookup_tool_id or lookup_tool_id in _ROOT_PRIMITIVE_TOOL_IDS:
|
|
6965
|
+
raw = LookupError(
|
|
6966
|
+
kind="error",
|
|
6967
|
+
reason=LookupErrorReason.invalid_params,
|
|
6968
|
+
message=(
|
|
6969
|
+
"find(mode='fetch') requires a concrete adapter tool_id "
|
|
6970
|
+
"from the current available adapter set. No concrete "
|
|
6971
|
+
"adapter was selected, so UMMAYA stopped this malformed "
|
|
6972
|
+
"tool call instead of retrying it."
|
|
6973
|
+
),
|
|
6974
|
+
retryable=False,
|
|
6975
|
+
)
|
|
6976
|
+
else:
|
|
6977
|
+
inp_lk = LookupFetchInput(
|
|
6978
|
+
mode="fetch",
|
|
6979
|
+
tool_id=lookup_tool_id,
|
|
6980
|
+
params=lookup_params,
|
|
6981
|
+
)
|
|
6982
|
+
raw = await find(
|
|
6983
|
+
inp_lk,
|
|
6984
|
+
registry=registry,
|
|
6985
|
+
executor=executor,
|
|
6986
|
+
session_identity=session_id,
|
|
6987
|
+
)
|
|
6530
6988
|
result_payload = {
|
|
6531
6989
|
"kind": "find",
|
|
6532
6990
|
"result": _serialize_primitive_result(raw),
|
|
@@ -6815,9 +7273,23 @@ async def run( # noqa: C901
|
|
|
6815
7273
|
# CC-style loop contract: the model can paint progress prose, then call
|
|
6816
7274
|
# a primitive dispatcher with a concrete adapter in `tool_id`.
|
|
6817
7275
|
registry = cast("Any", _ensure_tool_registry())
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
7276
|
+
turn_route_decision = _route_decision_for_turn(latest_user_utt)
|
|
7277
|
+
from ummaya.ipc.route_diagnostics import ( # noqa: PLC0415
|
|
7278
|
+
log_route_decision_diagnostic,
|
|
7279
|
+
)
|
|
7280
|
+
|
|
7281
|
+
log_route_decision_diagnostic(
|
|
7282
|
+
logger=logger,
|
|
7283
|
+
turn_index=_diag_turn_idx,
|
|
7284
|
+
session_id=frame.session_id,
|
|
7285
|
+
correlation_id=frame.correlation_id,
|
|
7286
|
+
decision=turn_route_decision,
|
|
7287
|
+
)
|
|
7288
|
+
turn_concrete_adapter_tools = _select_concrete_adapter_tools_for_turn(
|
|
7289
|
+
latest_user_utt, route_decision=turn_route_decision
|
|
7290
|
+
)
|
|
7291
|
+
turn_concrete_adapter_tool_ids = tuple(t.id for t in turn_concrete_adapter_tools)
|
|
7292
|
+
backend_tools_raw = [t.to_openai_tool() for t in turn_concrete_adapter_tools]
|
|
6821
7293
|
backend_tool_names: set[object] = set()
|
|
6822
7294
|
for raw_tool in backend_tools_raw:
|
|
6823
7295
|
if not isinstance(raw_tool, dict):
|
|
@@ -6951,7 +7423,11 @@ async def run( # noqa: C901
|
|
|
6951
7423
|
(latest_user_utt or "")[:256],
|
|
6952
7424
|
)
|
|
6953
7425
|
if latest_user_utt:
|
|
6954
|
-
suffix_block = _build_available_adapters_suffix(
|
|
7426
|
+
suffix_block = _build_available_adapters_suffix(
|
|
7427
|
+
latest_user_utt,
|
|
7428
|
+
route_decision=turn_route_decision,
|
|
7429
|
+
visible_tool_ids=turn_concrete_adapter_tool_ids,
|
|
7430
|
+
)
|
|
6955
7431
|
if suffix_block:
|
|
6956
7432
|
augmented_system = augmented_system + "\n\n" + suffix_block + "\n"
|
|
6957
7433
|
except Exception: # noqa: BLE001 — fail-open per FR-002
|
|
@@ -7024,6 +7500,7 @@ async def run( # noqa: C901
|
|
|
7024
7500
|
force_verify_next_turn: str | None = None
|
|
7025
7501
|
force_lookup_next_turn: str | None = None
|
|
7026
7502
|
force_submit_next_turn: str | None = None
|
|
7503
|
+
force_document_next_turn: str | None = None
|
|
7027
7504
|
force_no_tools_next_turn = False
|
|
7028
7505
|
continue_free_next_turn = False
|
|
7029
7506
|
mock_disclosure_required = False
|
|
@@ -7180,20 +7657,33 @@ async def run( # noqa: C901
|
|
|
7180
7657
|
force_submit_next_turn,
|
|
7181
7658
|
)
|
|
7182
7659
|
force_submit_next_turn = None
|
|
7660
|
+
elif (
|
|
7661
|
+
force_document_next_turn is not None
|
|
7662
|
+
and force_document_next_turn in _tool_definition_names(stream_tools)
|
|
7663
|
+
):
|
|
7664
|
+
stream_tool_choice = _function_tool_choice(force_document_next_turn)
|
|
7665
|
+
logger.warning(
|
|
7666
|
+
"_handle_chat_request: forcing concrete document adapter %s "
|
|
7667
|
+
"after validation gate",
|
|
7668
|
+
force_document_next_turn,
|
|
7669
|
+
)
|
|
7670
|
+
force_document_next_turn = None
|
|
7183
7671
|
elif (
|
|
7184
7672
|
force_locate_next_turn
|
|
7185
7673
|
or force_verify_next_turn is not None
|
|
7186
7674
|
or force_lookup_next_turn is not None
|
|
7187
7675
|
or force_submit_next_turn is not None
|
|
7676
|
+
or force_document_next_turn is not None
|
|
7188
7677
|
):
|
|
7189
7678
|
logger.warning(
|
|
7190
7679
|
"_handle_chat_request: continuing turn %d with free tool_choice "
|
|
7191
|
-
"after validation gate hint (locate=%s check=%s find=%s send=%s)",
|
|
7680
|
+
"after validation gate hint (locate=%s check=%s find=%s send=%s document=%s)",
|
|
7192
7681
|
_turn,
|
|
7193
7682
|
force_locate_next_turn,
|
|
7194
7683
|
force_verify_next_turn,
|
|
7195
7684
|
force_lookup_next_turn,
|
|
7196
7685
|
force_submit_next_turn,
|
|
7686
|
+
force_document_next_turn,
|
|
7197
7687
|
)
|
|
7198
7688
|
try:
|
|
7199
7689
|
stream_kwargs: dict[str, object] = {
|
|
@@ -7490,6 +7980,37 @@ async def run( # noqa: C901
|
|
|
7490
7980
|
buffered_visible.clear()
|
|
7491
7981
|
continue
|
|
7492
7982
|
|
|
7983
|
+
document_followup_gate = _check_document_workflow_terminated_without_required_tool(
|
|
7984
|
+
llm_messages,
|
|
7985
|
+
latest_user_utt,
|
|
7986
|
+
)
|
|
7987
|
+
if document_followup_gate is not None:
|
|
7988
|
+
document_tool_id = document_followup_gate["tool_id"]
|
|
7989
|
+
try:
|
|
7990
|
+
document_tool = _ensure_tool_registry().find(document_tool_id)
|
|
7991
|
+
document_primitive = str(getattr(document_tool, "primitive", "") or "")
|
|
7992
|
+
except Exception: # noqa: BLE001
|
|
7993
|
+
document_primitive = ""
|
|
7994
|
+
if document_primitive == "find":
|
|
7995
|
+
force_lookup_next_turn = document_tool_id
|
|
7996
|
+
elif document_primitive == "send":
|
|
7997
|
+
force_submit_next_turn = document_tool_id
|
|
7998
|
+
elif document_primitive == "check":
|
|
7999
|
+
force_verify_next_turn = document_tool_id
|
|
8000
|
+
elif document_primitive == "document":
|
|
8001
|
+
force_document_next_turn = document_tool_id
|
|
8002
|
+
else:
|
|
8003
|
+
continue_free_next_turn = True
|
|
8004
|
+
_append_tool_routing_observation(
|
|
8005
|
+
(
|
|
8006
|
+
"rejected final-answer turn — document workflow "
|
|
8007
|
+
f"missing {document_tool_id}"
|
|
8008
|
+
),
|
|
8009
|
+
document_followup_gate["message"],
|
|
8010
|
+
)
|
|
8011
|
+
buffered_visible.clear()
|
|
8012
|
+
continue
|
|
8013
|
+
|
|
7493
8014
|
from ummaya.llm.tool_call_parser import ( # noqa: PLC0415
|
|
7494
8015
|
strip_leaked_thinking_markers,
|
|
7495
8016
|
)
|
|
@@ -7503,9 +8024,46 @@ async def run( # noqa: C901
|
|
|
7503
8024
|
else:
|
|
7504
8025
|
merged_prose = _remove_unneeded_mock_disclosure(merged_prose)
|
|
7505
8026
|
merged_prose = _remove_unneeded_live_meta_disclosure(merged_prose)
|
|
8027
|
+
document_followup_gate = _check_document_workflow_terminated_without_required_tool(
|
|
8028
|
+
llm_messages,
|
|
8029
|
+
latest_user_utt,
|
|
8030
|
+
candidate_final_answer=merged_prose,
|
|
8031
|
+
)
|
|
8032
|
+
if document_followup_gate is not None:
|
|
8033
|
+
document_tool_id = document_followup_gate["tool_id"]
|
|
8034
|
+
try:
|
|
8035
|
+
document_tool = _ensure_tool_registry().find(document_tool_id)
|
|
8036
|
+
document_primitive = str(getattr(document_tool, "primitive", "") or "")
|
|
8037
|
+
except Exception: # noqa: BLE001
|
|
8038
|
+
document_primitive = ""
|
|
8039
|
+
if document_primitive == "find":
|
|
8040
|
+
force_lookup_next_turn = document_tool_id
|
|
8041
|
+
elif document_primitive == "send":
|
|
8042
|
+
force_submit_next_turn = document_tool_id
|
|
8043
|
+
elif document_primitive == "check":
|
|
8044
|
+
force_verify_next_turn = document_tool_id
|
|
8045
|
+
elif document_primitive == "document":
|
|
8046
|
+
force_document_next_turn = document_tool_id
|
|
8047
|
+
else:
|
|
8048
|
+
continue_free_next_turn = True
|
|
8049
|
+
_append_tool_routing_observation(
|
|
8050
|
+
(
|
|
8051
|
+
"rejected final-answer turn — document workflow "
|
|
8052
|
+
f"missing {document_tool_id}"
|
|
8053
|
+
),
|
|
8054
|
+
document_followup_gate["message"],
|
|
8055
|
+
)
|
|
8056
|
+
buffered_visible.clear()
|
|
8057
|
+
continue
|
|
7506
8058
|
has_successful_tool_result = _conversation_has_successful_any_primitive_result(
|
|
7507
8059
|
llm_messages
|
|
7508
8060
|
)
|
|
8061
|
+
diff_only_document_answer = _document_diff_only_final_answer(
|
|
8062
|
+
latest_user_utt,
|
|
8063
|
+
llm_messages,
|
|
8064
|
+
)
|
|
8065
|
+
if diff_only_document_answer is not None:
|
|
8066
|
+
merged_prose = diff_only_document_answer
|
|
7509
8067
|
if (
|
|
7510
8068
|
merged_prose.strip()
|
|
7511
8069
|
and _final_answer_looks_like_tool_call_narration(merged_prose)
|
|
@@ -7525,6 +8083,47 @@ async def run( # noqa: C901
|
|
|
7525
8083
|
)
|
|
7526
8084
|
buffered_visible.clear()
|
|
7527
8085
|
continue
|
|
8086
|
+
if (
|
|
8087
|
+
merged_prose.strip()
|
|
8088
|
+
and has_successful_tool_result
|
|
8089
|
+
and _final_answer_overclaims_document_edit(merged_prose, llm_messages)
|
|
8090
|
+
):
|
|
8091
|
+
if empty_final_retry_count < 2:
|
|
8092
|
+
empty_final_retry_count += 1
|
|
8093
|
+
_append_final_answer_observation(
|
|
8094
|
+
"rejected document final answer overclaimed observed diff",
|
|
8095
|
+
(
|
|
8096
|
+
"The previous assistant turn claimed document content or "
|
|
8097
|
+
"work sections not present in the latest document diff. "
|
|
8098
|
+
"Document diff changes are the only approved edit claims. "
|
|
8099
|
+
"Produce a concise Korean final answer using only "
|
|
8100
|
+
"result.status, text_summary, and diff.changes "
|
|
8101
|
+
"display_label/target_path/before_value/after_value from "
|
|
8102
|
+
"the latest document tool_result. Do not add activity content, "
|
|
8103
|
+
"achievements, next plans, problems, improvements, render "
|
|
8104
|
+
"artifacts, or document sections unless they appear in "
|
|
8105
|
+
"diff.changes."
|
|
8106
|
+
),
|
|
8107
|
+
)
|
|
8108
|
+
buffered_visible.clear()
|
|
8109
|
+
continue
|
|
8110
|
+
await write_frame(
|
|
8111
|
+
ErrorFrame(
|
|
8112
|
+
session_id=frame.session_id,
|
|
8113
|
+
correlation_id=frame.correlation_id or str(uuid.uuid4()),
|
|
8114
|
+
role="backend",
|
|
8115
|
+
ts=_utcnow(),
|
|
8116
|
+
kind="error",
|
|
8117
|
+
code="document_final_answer_overclaim",
|
|
8118
|
+
message=(
|
|
8119
|
+
"Model returned an ungrounded document final answer "
|
|
8120
|
+
"after successful document diff results. No synthetic "
|
|
8121
|
+
"answer was generated."
|
|
8122
|
+
),
|
|
8123
|
+
details={"retry_count": empty_final_retry_count},
|
|
8124
|
+
)
|
|
8125
|
+
)
|
|
8126
|
+
return
|
|
7528
8127
|
if not merged_prose.strip() and has_successful_tool_result:
|
|
7529
8128
|
if empty_final_retry_count < 2:
|
|
7530
8129
|
empty_final_retry_count += 1
|
|
@@ -8520,6 +9119,7 @@ async def run( # noqa: C901
|
|
|
8520
9119
|
or force_verify_next_turn is not None
|
|
8521
9120
|
or force_lookup_next_turn is not None
|
|
8522
9121
|
or force_submit_next_turn is not None
|
|
9122
|
+
or force_document_next_turn is not None
|
|
8523
9123
|
or continue_free_next_turn
|
|
8524
9124
|
):
|
|
8525
9125
|
if continue_free_next_turn:
|
|
@@ -8919,10 +9519,13 @@ async def run( # noqa: C901
|
|
|
8919
9519
|
user_query=_session_latest_user_utterances.get(frame.session_id, ""),
|
|
8920
9520
|
)
|
|
8921
9521
|
dispatch_args = _normalize_root_primitive_adapter_envelope(dispatch_name, dispatch_args)
|
|
9522
|
+
lookup_context = _session_latest_user_utterances.get(
|
|
9523
|
+
frame.session_id, ""
|
|
9524
|
+
) or _lookup_context_from_args(dispatch_args)
|
|
8922
9525
|
dispatch_args = _normalize_lookup_args_for_query(
|
|
8923
9526
|
dispatch_name,
|
|
8924
9527
|
dispatch_args,
|
|
8925
|
-
|
|
9528
|
+
lookup_context,
|
|
8926
9529
|
)
|
|
8927
9530
|
|
|
8928
9531
|
kma_aviation_choice_msg = _check_kma_aviation_tool_choice_prerequisite(
|
|
@@ -9145,6 +9748,35 @@ async def run( # noqa: C901
|
|
|
9145
9748
|
await _handle_tool_call(frame)
|
|
9146
9749
|
except Exception as exc: # noqa: BLE001
|
|
9147
9750
|
logger.exception("tool_call handler failed: %s", exc)
|
|
9751
|
+
try:
|
|
9752
|
+
from ummaya.ipc.frame_schema import ( # noqa: PLC0415
|
|
9753
|
+
ToolCallFrame,
|
|
9754
|
+
ToolResultEnvelope,
|
|
9755
|
+
ToolResultFrame,
|
|
9756
|
+
)
|
|
9757
|
+
from ummaya.primitives import PRIMITIVE_REGISTRY # noqa: PLC0415
|
|
9758
|
+
|
|
9759
|
+
if isinstance(frame, ToolCallFrame):
|
|
9760
|
+
error_kind = frame.name if frame.name in PRIMITIVE_REGISTRY else "find"
|
|
9761
|
+
await write_frame(
|
|
9762
|
+
ToolResultFrame(
|
|
9763
|
+
session_id=frame.session_id,
|
|
9764
|
+
correlation_id=frame.correlation_id,
|
|
9765
|
+
role="backend",
|
|
9766
|
+
ts=_utcnow(),
|
|
9767
|
+
kind="tool_result",
|
|
9768
|
+
call_id=frame.call_id,
|
|
9769
|
+
envelope=ToolResultEnvelope.model_validate(
|
|
9770
|
+
{
|
|
9771
|
+
"kind": error_kind,
|
|
9772
|
+
"error": f"tool_call handler failed: {exc}",
|
|
9773
|
+
"tool_id": frame.name,
|
|
9774
|
+
}
|
|
9775
|
+
),
|
|
9776
|
+
)
|
|
9777
|
+
)
|
|
9778
|
+
except Exception: # noqa: BLE001
|
|
9779
|
+
logger.exception("failed to emit tool_call failure result")
|
|
9148
9780
|
|
|
9149
9781
|
elif frame.kind == "permission_response":
|
|
9150
9782
|
# Spec 1978 T047 — resolve pending permission Future.
|