fruxon 2.0.2__tar.gz → 2.1.0__tar.gz
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.
- {fruxon-2.0.2 → fruxon-2.1.0}/HISTORY.md +29 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/PKG-INFO +4 -1
- {fruxon-2.0.2 → fruxon-2.1.0}/README.md +3 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/__init__.py +6 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/_version.py +2 -2
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/__init__.py +1 -0
- fruxon-2.1.0/src/fruxon/cli/agents_voice_calls.py +379 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/examples.py +15 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/llm_providers.py +261 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/triggers_ledger.py +144 -3
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/fruxon.py +191 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/models.py +89 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/skills/fruxon-build-agent/SKILL.md +6 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/skills/fruxon-debug-trace/SKILL.md +7 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/skills/fruxon-meet/SKILL.md +2 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_client.py +47 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_llm_provider_configs_cli.py +255 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_triggers_ledger.py +147 -0
- fruxon-2.1.0/tests/test_voice_calls.py +288 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/.gitignore +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/LICENSE +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/pyproject.toml +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/__main__.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/_ssl.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/checks/__init__.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/checks/agent.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/checks/ledger.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/checks/references.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/_crash.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/_schema.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/_shared.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/_stream.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_approvals.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_budget.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_channels.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_draft.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_evaluations.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_executions.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_improvements.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_lifecycle.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_memory.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_reply_style.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_revisions.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_sandbox.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_sandbox_test.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_slots.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_system_blocks.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_tests.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/agents_topics.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/applications.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/applications_entry_points.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/assets.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/auth.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/capabilities.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/completion.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/config.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/consult_pins.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/conversation_imports.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/dash.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/describe.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/doctor.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/environments.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/escalations.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/guides.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/integrations.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/keys.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/knowledge_bases.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/knowledge_documents.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/knowledge_notices.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/knowledge_sources.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/messages.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/metrics.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/participants.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/pipelines.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/secrets.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/skills.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/storage.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/tools.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/triggers.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/triggers_budget.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/triggers_check.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/triggers_ledger_stage_tests.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/triggers_questions.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/triggers_shape.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/cli/workspaces.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/credentials.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/device_auth.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/doctor.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/exceptions.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/flow_validation.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/output.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/params.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/sandbox_scenario.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/skills/__init__.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/skills/fruxon-agent-mode/SKILL.md +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/skills/fruxon-create-integration/SKILL.md +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/skills/fruxon-use-integrations/SKILL.md +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/telemetry.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/__init__.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/app.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/demo.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/links.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/screens/__init__.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/screens/agent.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/screens/base.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/screens/home.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/screens/ledger.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/screens/loader.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/screens/pickers.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/screens/trace.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/source.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/theme.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/views.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/tui/widgets.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/ui.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/update_check.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/src/fruxon/validation.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/.test_durations.json +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/__init__.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/conftest.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_actor.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_agent_lifecycle.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_agent_revisions_list.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_agent_slots.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_agents_check_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_agents_revisions_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_agents_sandbox.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_applications.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_approvals.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_assets.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_budgets.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_capabilities.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_channels.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_checks_agent.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_checks_ledger.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_cli_base_url_routing.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_cli_crash.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_cli_root_flags.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_connect_nudge.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_consult.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_conversation_imports.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_credentials.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_dash_app.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_dash_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_dash_client.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_dash_links.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_dash_source.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_describe.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_device_auth.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_docs_commands_exist.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_doctor.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_draft_bootstrap.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_draft_evaluate_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_draft_pull_shape.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_draft_run_replay.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_draft_validate_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_drafts.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_entry_points.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_environments.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_escalations.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_evaluations.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_execution_records.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_flow_validation.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_fruxon.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_guides.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_improvements.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_integration_configs_write_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_integration_triggers_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_integrations_authorize_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_integrations_connect_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_kb_trial_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_knowledge_bases.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_knowledge_documents.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_knowledge_notices.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_knowledge_sources.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_llm_providers_connect_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_managed_by.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_memory.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_messages.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_metrics.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_online_llm_config_refs.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_online_prefetch_refs.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_online_secret_refs.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_output.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_params.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_participants.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_participants_write.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_pipelines.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_reply_style.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_sandbox_scenario.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_schema.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_schema_fetch.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_secrets.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_skills.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_ssl.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_storage.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_system_blocks.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_system_blocks_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_telemetry.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_test_chats.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_topics.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_trigger_budgets.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_triggers.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_triggers_check_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_triggers_ledger_batch_views.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_triggers_ledger_stage_tests.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_triggers_questions.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_triggers_shape.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_triggers_write.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_tui_boundary.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_ui.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_update_check.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_validation.py +0 -0
- {fruxon-2.0.2 → fruxon-2.1.0}/tests/test_workspaces_cli.py +0 -0
|
@@ -1,5 +1,34 @@
|
|
|
1
1
|
# History
|
|
2
2
|
|
|
3
|
+
## Unreleased — read voice calls back
|
|
4
|
+
|
|
5
|
+
- **`fruxon agents voice-calls list <agent>`** lists the agent's Talk calls
|
|
6
|
+
from the Test lab, newest first. Each row has the status, language, why the
|
|
7
|
+
call ended, its turns and its length. Output is `table`, `json` or `id`.
|
|
8
|
+
- **`fruxon agents voice-calls get <agent> <call-id>`** shows one call turn by
|
|
9
|
+
turn: what the caller said, the whole reply, and what was heard of it. When
|
|
10
|
+
the caller cut in, `delivery` is `INTERRUPTED` and `spokenText` is the part
|
|
11
|
+
they heard. Each turn also carries `generation` and the run behind its reply
|
|
12
|
+
(`executionRecordId`, for `agents executions trace`). Output is `text`,
|
|
13
|
+
`table` or `json`.
|
|
14
|
+
- Client methods `list_voice_calls` and `get_voice_call` return the new
|
|
15
|
+
`VoiceCall` and `VoiceCallTurn`. Both need fruxon-backend #1537; an older
|
|
16
|
+
server answers 404.
|
|
17
|
+
|
|
18
|
+
## Unreleased — one completion on an LLM config, with no agent
|
|
19
|
+
|
|
20
|
+
- **`fruxon llm-providers configs complete <provider> <config> --model M`**
|
|
21
|
+
sends a system prompt and a short conversation to a model through the
|
|
22
|
+
config's published revision and prints the reply, with no agent, session or
|
|
23
|
+
execution record (scope `llm_providers:test`; fruxon-backend `:complete`).
|
|
24
|
+
It answers "does this model follow that instruction?" — pass a platform
|
|
25
|
+
system block's text as `--system` and read the reply — without a test turn
|
|
26
|
+
that writes to a live agent. `--system` (text, `@file`, `-`; repeatable),
|
|
27
|
+
`-m [user:|assistant:]TEXT` (repeatable) or `--messages chat.json`,
|
|
28
|
+
`--temperature`, `--max-tokens` (server cap 8192), `--param k=v`. The reply
|
|
29
|
+
goes to stdout, model, finish reason, tokens and latency to stderr; `-o json`
|
|
30
|
+
passes the server's result through. A Fruxon-hosted provider is refused.
|
|
31
|
+
|
|
3
32
|
## Unreleased — `fruxon messages list --reason EMAIL_THREAD_UNAVAILABLE`
|
|
4
33
|
|
|
5
34
|
- `--reason` takes the server's new `EMAIL_THREAD_UNAVAILABLE` (fruxon-backend
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fruxon
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: The Fruxon SDK is a lightweight Python client for integrating with the Fruxon platform.
|
|
5
5
|
Project-URL: bugs, https://github.com/fruxon-ai/fruxon-sdk/issues
|
|
6
6
|
Project-URL: changelog, https://github.com/fruxon-ai/fruxon-sdk/blob/main/HISTORY.md
|
|
@@ -104,6 +104,7 @@ Execution:
|
|
|
104
104
|
| `fruxon escalations list/history/unhandled/policy/ack` | What happened when an agent handed a conversation to a person. `list` is the queue (live takeovers + degraded escalations nobody was reachable for); `unhandled` is the append-only ledger under it. **`history` is the record of closed console-routed takeovers** — those leave the queue, so `list --status ALL` answers `[]` about a conversation a human worked for half an hour. Read `humanObservedAt` before believing a `RESOLVED`: null means nobody was ever seen picking it up. `policy <agent>` resolves who the *next* escalation would reach — an empty ladder under `NATIVE_HANDOFF` is correct, the console supplies the human. |
|
|
105
105
|
| `fruxon messages list` | The channel ledger — including inbound an agent was never shown. A **withheld** message (a human owns the chat in the source console, or admission refused the sender) never becomes a conversation turn, so it is in no transcript and the thread reads as a customer being ignored. `--delivery WITHHELD_PENDING` lists the ones nothing has ever acted on; nothing drains them. |
|
|
106
106
|
| `fruxon agents sandbox open/turn/fire/resolve-input/stream/answer/close <session>` | Drive an agent-network sandbox end-to-end without real channels — send turns as participants, test-fire triggers, and watch the run (`stream`, NDJSON in agent mode). Sandbox **captures the reply and hard-blocks every real send**; `--await` makes consult/approval gates suspend so they can be resolved (`answer` for consults, `approvals respond` for approvals). `--draft` runs your unpublished draft instead of the deployed revision (`sandbox draft <session>` says which); `--confirm-writes` holds every create/update/delete tool call for approval, because sandbox does **not** block integration writes; a write that did not run (rejected, expired, or one the run could not pause for) shows on `stream` as `refused`. |
|
|
107
|
+
| `fruxon agents voice-calls list/get <agent>` | Read back the Test lab's Talk calls. `list` gives calls newest first (status, end reason, turns, length). `get <agent> <call-id>` gives the call turn by turn: the caller's transcript, the reply, what was heard of it (`delivery`; `spokenText` when the caller cut in), and each reply's `executionRecordId` for `agents executions trace`. Read-only; needs `executions:read`. |
|
|
107
108
|
| `fruxon agents sandbox fork <agent> <message-id>` | Fork a real conversation at a transcript row (an `id` from `agents topics messages`) into a sandbox session already holding the history the agent had when it produced that reply. `--send` re-sends the customer's message against it — with `--draft` to test an edit before publishing — and holds writes for approval unless `--no-confirm-writes`. |
|
|
108
109
|
| `fruxon agents sandbox test <file-or-dir>` | Run scenario-based e2e tests of an agent's network behavior. A scenario (YAML/JSON) declares `steps` (turns / trigger fires), `responders` (scripted consult/approval answers), and `expect` assertions (reply / routing / tools called or refused / no-leak / outcome); the runner drives a sandbox session and returns one verdict — exit 0 (pass) / 1 (fail) / 12 (bad file), with `--junit` for CI. An optional `setup` block provisions real resources first — an `asset` (uploaded + ingestion-waited), `participant`, or `agent` — templated as `${asset.catalog.id}` and **torn down afterwards even on failure** (`--keep` to inspect). Assertions are deterministic by default; `reply_judge: {metric, min_score}` adds an opt-in **semantic** check that scores the reply against a tenant eval metric. The thing an agent runs to prove the agent it built works. |
|
|
109
110
|
|
|
@@ -151,6 +152,7 @@ Integrations + tools:
|
|
|
151
152
|
| `fruxon llm-providers list/get/models` | Browse LLM providers and models supported at the tenant level. |
|
|
152
153
|
| `fruxon llm-providers connect <provider>` / `link-status` | Create a config for an LLM provider and print a **credential link**: a human opens it, signs in to Fruxon, types the API key (or access keys, or service account key), and the page tests it with the provider and publishes the config an agent pins as `provider.tenantConfigId`. The key never passes through the terminal or an agent driving the CLI. `--config <id>` fills an existing config, `--param name=value` sets a non-secret setting such as Bedrock's `region`, `--no-sign-in` mints a link anyone holding it can fill (Admin or Owner). `link-status <provider> <config> <request> [--wait]` says when it was filled. |
|
|
153
154
|
| `fruxon llm-providers configs list/get/test/publish` | The workspace's saved LLM credentials — the `provider.tenantConfigId` an agent pins. `get` shows the draft (secrets masked), the published revision and last known health. `test` probes the draft on every provider surface that shares the credential (or just one, given that surface's provider id), one line per failing surface, exit 12 on failure (`llm_providers:test`). `publish` ships the draft as a new revision that every agent on the config switches to — it confirms, needs `--yes` in agent mode, and does not wait on a passing test (`llm_providers:write`). |
|
|
155
|
+
| `fruxon llm-providers configs complete <provider> <config> --model M` | One completion on a saved config's published revision, with no agent, session or execution — to see whether a model follows a prompt or a system block's instruction. `--system` (text, `@file`, `-`; repeatable), `-m [user:\|assistant:]TEXT` (repeatable) or `--messages chat.json`, `--temperature`, `--max-tokens` (server cap 8192), `--param k=v`. The reply goes to stdout, the model, finish reason, tokens and latency to stderr (`llm_providers:test`). Fruxon-hosted providers are refused. |
|
|
154
156
|
| `fruxon assets create/list/get/wait/operations/delete` | Manage knowledge-base (RAG) assets an agent can query — upload local files, wait for async ingestion, inspect operations, and use the ids for `assetConfig.assetIds`. `delete` confirms + refuses without `--yes` in agent mode. |
|
|
155
157
|
| `fruxon knowledge-bases list/get/create/update/publish/archive/delete` | The authored, reviewable corpus behind a `manual_knowledge` asset — **not** the asset itself, which is what gets searched (`get` prints the asset id as `backingAssetId`). **`publish` is the step that makes any of it findable**: only `PUBLISHED` documents travel in a snapshot, and only publishing the *base* moves one, so a base full of finished articles answers nothing until it runs. It returns on *acceptance*, not searchability — RAG ingests afterwards, so a pending publish state is normal. `list` hides archived bases, which keep answering for agents already attached to them; `delete` is refused while an agent revision still attaches one, and `archive` is the reversible path. |
|
|
156
158
|
| `fruxon knowledge-bases usage <kb>` | Whether agents actually use the base: searches per hour, day or week (`--interval`), the passage tokens each search handed the agent, how many found nothing (split from searches that **could not fully search** it — index missing or still building, a phrasing failed), and which agents searched (up to 20 you can open, the rest counted together). `--origin test` counts test runs instead of production. Retrieved tokens are counted by the search index, so what the model read can be less when a large result is trimmed or offloaded. Searches are recorded from 15 October 2026 and kept 90 days at most, so a window reaching further back says where counting began rather than showing empty days. A 404 is a wrong id or a backend that predates the read. |
|
|
@@ -170,6 +172,7 @@ Integrations + tools:
|
|
|
170
172
|
| `fruxon triggers check <trigger>` | Audit whether a work-shape automation can move its items, part by part in the order an item meets them: on and taking items in, the last pass read its source, each PROCESS stage's agent exists with a deployed revision and working credentials and models (a rejected credential's row says how many items failed there since), each ASK stage's contact is declared and bound on the asking agent and its oldest question has not waited past a fifth of its answer window, the outcome note's contact reaches a person, this week's spend against the weekly limit, and how many items died in the last 7 days. `{trigger, overall, checks[]}` like `agents check`; exits 12 only on a hard fail, warnings stay 0. Read-only. |
|
|
171
173
|
| `fruxon triggers budget list/get/set/delete` | Spend limits on an automation **as a whole** (`--period DAILY/WEEKLY/MONTHLY`, UTC calendar windows), counting every production run it caused whichever agent made it. Once one is enforced they govern its runs, which then meet only these limits and not the agents' own; with none enforced, each stage stops at its agent's own limit. None by default and every one removable. An enforced limit refuses new work at the limit (stage runs, passes, a question's opening, the report) and holds the stages until the window resets, while turns of conversations it already opened still run (up to 110%); `--no-enforce` only alerts the Application's admins. Needs Editor. |
|
|
172
174
|
| `fruxon triggers ledger redrive/ignore/settle/cancel-batch` | The operator verbs; all confirm and refuse without `--yes` in agent mode. `redrive` returns a DEAD or FAILED item to the queue with its attempts reset — settle any `UNKNOWN` effect first or the next attempt hits the same refusal. `ignore` excludes an item for a **required** reason, which is the only thing separating IGNORED from DEAD later. `settle` answers an effect the platform could not confirm (`--landed` / `--did-not-land` + `--reason`); wrong in the second direction duplicates the write. `cancel-batch` calls off a question — the items go back to READY and a later pass re-batches them. |
|
|
175
|
+
| `fruxon triggers ledger admit` | Start one item by hand on an automation whose work shape declares `onDemand`. `--set path=value` / `path:=<json>` (dotted paths nest) or `--data-file`; the server checks the declared inputs and refuses credential-named keys. The key — `--key`, or composed from the shape's `keyPaths` — is the whole idempotency: the same key again returns the existing item **unchanged** (`created: false`), whatever its status. Needs `triggers:fire`. |
|
|
173
176
|
| `fruxon triggers ledger set-data` | Correct a parked (DEAD / FAILED / IGNORED) item's `data` before re-driving it — the value a stage recorded wrong, such as an ASK answer written as `SKIP`, which a plain re-drive would route on again. `--set path=value` (or `path:=<json>`, repeatable) on a top-level key the item carries or a path the shape declares (`produces`, `answeredWhenPath`); a misspelled key is refused with the settable list rather than added. `--reason` is required and lands on the item's history beside every old → new value. `--redrive` re-drives in the same transaction; `redrive` and `redrive-many` take `--set` too. |
|
|
174
177
|
| `fruxon triggers ledger redrive-many/ignore-many` | The same two verbs over a **selection** — `--id` (repeatable), `--stage`, `--status`, `--before`/`--after` (Unix ms). The CLI resolves the selection to a count under the verb's own eligibility, shows it, and the server acts on exactly that count (409 if the set changed). The count is the server's own whenever the endpoint's filters say exactly what the selection says (a `--status` or an `--ignore-reason`, with no `--id` and no `--before`/`--after`) — one request instead of walking every page of a 4,000-item ledger, and one fewer place for the two sides to disagree. `ignore-many --stage X` is how you drain a stage. When the selection is `--id` values, the result names every id it did **not** act on and why — not a work item, another automation's, or a status the verb cannot touch (usually a pass holding the claim). |
|
|
175
178
|
| `fruxon triggers ledger exclusions` | Every reason items were set aside, with how many each holds — the list `--ignore-reason` is picked from, and usually the fastest way to see a prompt excluding a whole class of work it should not. `funnel` stops at `1,204 IGNORED`, which is a status and not something anyone can act on; an exclusion is a judgement a stage made under an instruction, so what you hold after revising it is a *reason*. Each count is exactly what a reason-scoped re-drive resolves, so the diagnosis and the fix use the same string. Reasons print whole and are never truncated — the filter matches the entire string, case-sensitively, so a shortened one selects nothing. A `(none recorded)` row predates the reason being kept on the item: those items are real, no reason filter can reach them, and their way back is `--id`. At most 200 distinct reasons come back. |
|
|
@@ -69,6 +69,7 @@ Execution:
|
|
|
69
69
|
| `fruxon escalations list/history/unhandled/policy/ack` | What happened when an agent handed a conversation to a person. `list` is the queue (live takeovers + degraded escalations nobody was reachable for); `unhandled` is the append-only ledger under it. **`history` is the record of closed console-routed takeovers** — those leave the queue, so `list --status ALL` answers `[]` about a conversation a human worked for half an hour. Read `humanObservedAt` before believing a `RESOLVED`: null means nobody was ever seen picking it up. `policy <agent>` resolves who the *next* escalation would reach — an empty ladder under `NATIVE_HANDOFF` is correct, the console supplies the human. |
|
|
70
70
|
| `fruxon messages list` | The channel ledger — including inbound an agent was never shown. A **withheld** message (a human owns the chat in the source console, or admission refused the sender) never becomes a conversation turn, so it is in no transcript and the thread reads as a customer being ignored. `--delivery WITHHELD_PENDING` lists the ones nothing has ever acted on; nothing drains them. |
|
|
71
71
|
| `fruxon agents sandbox open/turn/fire/resolve-input/stream/answer/close <session>` | Drive an agent-network sandbox end-to-end without real channels — send turns as participants, test-fire triggers, and watch the run (`stream`, NDJSON in agent mode). Sandbox **captures the reply and hard-blocks every real send**; `--await` makes consult/approval gates suspend so they can be resolved (`answer` for consults, `approvals respond` for approvals). `--draft` runs your unpublished draft instead of the deployed revision (`sandbox draft <session>` says which); `--confirm-writes` holds every create/update/delete tool call for approval, because sandbox does **not** block integration writes; a write that did not run (rejected, expired, or one the run could not pause for) shows on `stream` as `refused`. |
|
|
72
|
+
| `fruxon agents voice-calls list/get <agent>` | Read back the Test lab's Talk calls. `list` gives calls newest first (status, end reason, turns, length). `get <agent> <call-id>` gives the call turn by turn: the caller's transcript, the reply, what was heard of it (`delivery`; `spokenText` when the caller cut in), and each reply's `executionRecordId` for `agents executions trace`. Read-only; needs `executions:read`. |
|
|
72
73
|
| `fruxon agents sandbox fork <agent> <message-id>` | Fork a real conversation at a transcript row (an `id` from `agents topics messages`) into a sandbox session already holding the history the agent had when it produced that reply. `--send` re-sends the customer's message against it — with `--draft` to test an edit before publishing — and holds writes for approval unless `--no-confirm-writes`. |
|
|
73
74
|
| `fruxon agents sandbox test <file-or-dir>` | Run scenario-based e2e tests of an agent's network behavior. A scenario (YAML/JSON) declares `steps` (turns / trigger fires), `responders` (scripted consult/approval answers), and `expect` assertions (reply / routing / tools called or refused / no-leak / outcome); the runner drives a sandbox session and returns one verdict — exit 0 (pass) / 1 (fail) / 12 (bad file), with `--junit` for CI. An optional `setup` block provisions real resources first — an `asset` (uploaded + ingestion-waited), `participant`, or `agent` — templated as `${asset.catalog.id}` and **torn down afterwards even on failure** (`--keep` to inspect). Assertions are deterministic by default; `reply_judge: {metric, min_score}` adds an opt-in **semantic** check that scores the reply against a tenant eval metric. The thing an agent runs to prove the agent it built works. |
|
|
74
75
|
|
|
@@ -116,6 +117,7 @@ Integrations + tools:
|
|
|
116
117
|
| `fruxon llm-providers list/get/models` | Browse LLM providers and models supported at the tenant level. |
|
|
117
118
|
| `fruxon llm-providers connect <provider>` / `link-status` | Create a config for an LLM provider and print a **credential link**: a human opens it, signs in to Fruxon, types the API key (or access keys, or service account key), and the page tests it with the provider and publishes the config an agent pins as `provider.tenantConfigId`. The key never passes through the terminal or an agent driving the CLI. `--config <id>` fills an existing config, `--param name=value` sets a non-secret setting such as Bedrock's `region`, `--no-sign-in` mints a link anyone holding it can fill (Admin or Owner). `link-status <provider> <config> <request> [--wait]` says when it was filled. |
|
|
118
119
|
| `fruxon llm-providers configs list/get/test/publish` | The workspace's saved LLM credentials — the `provider.tenantConfigId` an agent pins. `get` shows the draft (secrets masked), the published revision and last known health. `test` probes the draft on every provider surface that shares the credential (or just one, given that surface's provider id), one line per failing surface, exit 12 on failure (`llm_providers:test`). `publish` ships the draft as a new revision that every agent on the config switches to — it confirms, needs `--yes` in agent mode, and does not wait on a passing test (`llm_providers:write`). |
|
|
120
|
+
| `fruxon llm-providers configs complete <provider> <config> --model M` | One completion on a saved config's published revision, with no agent, session or execution — to see whether a model follows a prompt or a system block's instruction. `--system` (text, `@file`, `-`; repeatable), `-m [user:\|assistant:]TEXT` (repeatable) or `--messages chat.json`, `--temperature`, `--max-tokens` (server cap 8192), `--param k=v`. The reply goes to stdout, the model, finish reason, tokens and latency to stderr (`llm_providers:test`). Fruxon-hosted providers are refused. |
|
|
119
121
|
| `fruxon assets create/list/get/wait/operations/delete` | Manage knowledge-base (RAG) assets an agent can query — upload local files, wait for async ingestion, inspect operations, and use the ids for `assetConfig.assetIds`. `delete` confirms + refuses without `--yes` in agent mode. |
|
|
120
122
|
| `fruxon knowledge-bases list/get/create/update/publish/archive/delete` | The authored, reviewable corpus behind a `manual_knowledge` asset — **not** the asset itself, which is what gets searched (`get` prints the asset id as `backingAssetId`). **`publish` is the step that makes any of it findable**: only `PUBLISHED` documents travel in a snapshot, and only publishing the *base* moves one, so a base full of finished articles answers nothing until it runs. It returns on *acceptance*, not searchability — RAG ingests afterwards, so a pending publish state is normal. `list` hides archived bases, which keep answering for agents already attached to them; `delete` is refused while an agent revision still attaches one, and `archive` is the reversible path. |
|
|
121
123
|
| `fruxon knowledge-bases usage <kb>` | Whether agents actually use the base: searches per hour, day or week (`--interval`), the passage tokens each search handed the agent, how many found nothing (split from searches that **could not fully search** it — index missing or still building, a phrasing failed), and which agents searched (up to 20 you can open, the rest counted together). `--origin test` counts test runs instead of production. Retrieved tokens are counted by the search index, so what the model read can be less when a large result is trimmed or offloaded. Searches are recorded from 15 October 2026 and kept 90 days at most, so a window reaching further back says where counting began rather than showing empty days. A 404 is a wrong id or a backend that predates the read. |
|
|
@@ -135,6 +137,7 @@ Integrations + tools:
|
|
|
135
137
|
| `fruxon triggers check <trigger>` | Audit whether a work-shape automation can move its items, part by part in the order an item meets them: on and taking items in, the last pass read its source, each PROCESS stage's agent exists with a deployed revision and working credentials and models (a rejected credential's row says how many items failed there since), each ASK stage's contact is declared and bound on the asking agent and its oldest question has not waited past a fifth of its answer window, the outcome note's contact reaches a person, this week's spend against the weekly limit, and how many items died in the last 7 days. `{trigger, overall, checks[]}` like `agents check`; exits 12 only on a hard fail, warnings stay 0. Read-only. |
|
|
136
138
|
| `fruxon triggers budget list/get/set/delete` | Spend limits on an automation **as a whole** (`--period DAILY/WEEKLY/MONTHLY`, UTC calendar windows), counting every production run it caused whichever agent made it. Once one is enforced they govern its runs, which then meet only these limits and not the agents' own; with none enforced, each stage stops at its agent's own limit. None by default and every one removable. An enforced limit refuses new work at the limit (stage runs, passes, a question's opening, the report) and holds the stages until the window resets, while turns of conversations it already opened still run (up to 110%); `--no-enforce` only alerts the Application's admins. Needs Editor. |
|
|
137
139
|
| `fruxon triggers ledger redrive/ignore/settle/cancel-batch` | The operator verbs; all confirm and refuse without `--yes` in agent mode. `redrive` returns a DEAD or FAILED item to the queue with its attempts reset — settle any `UNKNOWN` effect first or the next attempt hits the same refusal. `ignore` excludes an item for a **required** reason, which is the only thing separating IGNORED from DEAD later. `settle` answers an effect the platform could not confirm (`--landed` / `--did-not-land` + `--reason`); wrong in the second direction duplicates the write. `cancel-batch` calls off a question — the items go back to READY and a later pass re-batches them. |
|
|
140
|
+
| `fruxon triggers ledger admit` | Start one item by hand on an automation whose work shape declares `onDemand`. `--set path=value` / `path:=<json>` (dotted paths nest) or `--data-file`; the server checks the declared inputs and refuses credential-named keys. The key — `--key`, or composed from the shape's `keyPaths` — is the whole idempotency: the same key again returns the existing item **unchanged** (`created: false`), whatever its status. Needs `triggers:fire`. |
|
|
138
141
|
| `fruxon triggers ledger set-data` | Correct a parked (DEAD / FAILED / IGNORED) item's `data` before re-driving it — the value a stage recorded wrong, such as an ASK answer written as `SKIP`, which a plain re-drive would route on again. `--set path=value` (or `path:=<json>`, repeatable) on a top-level key the item carries or a path the shape declares (`produces`, `answeredWhenPath`); a misspelled key is refused with the settable list rather than added. `--reason` is required and lands on the item's history beside every old → new value. `--redrive` re-drives in the same transaction; `redrive` and `redrive-many` take `--set` too. |
|
|
139
142
|
| `fruxon triggers ledger redrive-many/ignore-many` | The same two verbs over a **selection** — `--id` (repeatable), `--stage`, `--status`, `--before`/`--after` (Unix ms). The CLI resolves the selection to a count under the verb's own eligibility, shows it, and the server acts on exactly that count (409 if the set changed). The count is the server's own whenever the endpoint's filters say exactly what the selection says (a `--status` or an `--ignore-reason`, with no `--id` and no `--before`/`--after`) — one request instead of walking every page of a 4,000-item ledger, and one fewer place for the two sides to disagree. `ignore-many --stage X` is how you drain a stage. When the selection is `--id` values, the result names every id it did **not** act on and why — not a work item, another automation's, or a status the verb cannot touch (usually a pass holding the claim). |
|
|
140
143
|
| `fruxon triggers ledger exclusions` | Every reason items were set aside, with how many each holds — the list `--ignore-reason` is picked from, and usually the fastest way to see a prompt excluding a whole class of work it should not. `funnel` stops at `1,204 IGNORED`, which is a status and not something anyone can act on; an exclusion is a judgement a stage made under an instruction, so what you hold after revising it is a *reason*. Each count is exactly what a reason-scoped re-drive resolves, so the diagnosis and the fix use the same string. Reasons print whole and are never truncated — the filter matches the entire string, case-sensitively, so a shortened one selects nothing. A `(none recorded)` row predates the reason being kept on the item: those items are real, no reason filter can reach them, and their way back is `--id`. At most 200 distinct reasons come back. |
|
|
@@ -80,6 +80,7 @@ from fruxon.models import (
|
|
|
80
80
|
LedgerFunnelCell,
|
|
81
81
|
LedgerGatheringGroup,
|
|
82
82
|
LedgerItem,
|
|
83
|
+
LedgerItemAdmission,
|
|
83
84
|
LedgerItemPoint,
|
|
84
85
|
LedgerItemTimeseries,
|
|
85
86
|
LedgerItemTransition,
|
|
@@ -123,6 +124,8 @@ from fruxon.models import (
|
|
|
123
124
|
TriggerParameterMapping,
|
|
124
125
|
TriggerRevision,
|
|
125
126
|
UnhandledEscalation,
|
|
127
|
+
VoiceCall,
|
|
128
|
+
VoiceCallTurn,
|
|
126
129
|
WorkShapeDoor,
|
|
127
130
|
WorkShapeDoorConversation,
|
|
128
131
|
WorkShapeDoorField,
|
|
@@ -212,6 +215,7 @@ __all__ = [
|
|
|
212
215
|
"LedgerFunnelCell",
|
|
213
216
|
"LedgerGatheringGroup",
|
|
214
217
|
"LedgerItem",
|
|
218
|
+
"LedgerItemAdmission",
|
|
215
219
|
"LedgerItemTransition",
|
|
216
220
|
"LedgerPass",
|
|
217
221
|
"LedgerPurgeResult",
|
|
@@ -250,6 +254,8 @@ __all__ = [
|
|
|
250
254
|
"TriggerRevision",
|
|
251
255
|
"UnhandledEscalation",
|
|
252
256
|
"ValidationError",
|
|
257
|
+
"VoiceCall",
|
|
258
|
+
"VoiceCallTurn",
|
|
253
259
|
"WorkShapeDoor",
|
|
254
260
|
"WorkShapeDoorConversation",
|
|
255
261
|
"WorkShapeDoorField",
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.0
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.1.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 1, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -710,6 +710,7 @@ from fruxon.cli import agents_slots as _agents_slots # noqa: E402, F401
|
|
|
710
710
|
from fruxon.cli import agents_system_blocks as _agents_system_blocks # noqa: E402, F401
|
|
711
711
|
from fruxon.cli import agents_tests as _agents_tests # noqa: E402, F401
|
|
712
712
|
from fruxon.cli import agents_topics as _agents_topics # noqa: E402, F401
|
|
713
|
+
from fruxon.cli import agents_voice_calls as _agents_voice_calls # noqa: E402, F401
|
|
713
714
|
from fruxon.cli import applications as _applications # noqa: E402, F401
|
|
714
715
|
from fruxon.cli import applications_entry_points as _applications_entry_points # noqa: E402, F401
|
|
715
716
|
from fruxon.cli import assets as _assets # noqa: E402, F401
|
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""``fruxon agents voice-calls`` — read an agent's voice (Talk) calls back.
|
|
2
|
+
|
|
3
|
+
A voice call is a Talk press in the studio Test lab: the caller speaks, the
|
|
4
|
+
voice worker transcribes, the agent answers, and the worker speaks the reply.
|
|
5
|
+
Each turn records two things a text transcript cannot: whether the reply ran
|
|
6
|
+
to the end (``generation``) and how much of it the caller actually heard
|
|
7
|
+
(``delivery``, with ``spokenText`` when they cut in). This is the read surface
|
|
8
|
+
over them — "what did the caller say, what did the agent answer, what was
|
|
9
|
+
heard of it" — without reading the cell's database.
|
|
10
|
+
|
|
11
|
+
Agent-nested like executions (the backend's
|
|
12
|
+
``agents/{agent}/voiceSessions``, ``executions:read``), and every turn names
|
|
13
|
+
its run, so ``fruxon agents executions trace <agent> <execution-id>`` follows
|
|
14
|
+
on from any reply.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
from typing import Annotated
|
|
21
|
+
|
|
22
|
+
import typer
|
|
23
|
+
from rich.markup import escape as markup_escape
|
|
24
|
+
|
|
25
|
+
from fruxon.cli._shared import build_client, format_ts
|
|
26
|
+
from fruxon.cli.agents import _hint_for_get_error, agents_app
|
|
27
|
+
from fruxon.exceptions import FruxonError, NotFoundError
|
|
28
|
+
from fruxon.fruxon import FruxonClient, VoiceCall, VoiceCallTurn
|
|
29
|
+
from fruxon.ui import (
|
|
30
|
+
EXIT_VALIDATION,
|
|
31
|
+
fail,
|
|
32
|
+
fail_from_api_error,
|
|
33
|
+
format_duration,
|
|
34
|
+
print_banner,
|
|
35
|
+
resolve_output_format,
|
|
36
|
+
say_info,
|
|
37
|
+
say_ok,
|
|
38
|
+
stderr,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
voice_calls_app = typer.Typer(
|
|
42
|
+
help="Read an agent's voice (Talk) calls back: what was said, the reply, what was heard (list / get).",
|
|
43
|
+
no_args_is_help=True,
|
|
44
|
+
)
|
|
45
|
+
agents_app.add_typer(voice_calls_app, name="voice-calls")
|
|
46
|
+
|
|
47
|
+
# How much of a transcript or reply a table cell shows; `get -o text` / `-o json` show all of it.
|
|
48
|
+
_CELL_CHARS = 60
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@voice_calls_app.command("list")
|
|
52
|
+
def voice_calls_list(
|
|
53
|
+
agent: Annotated[str, typer.Argument(help="Agent whose voice calls to list.")],
|
|
54
|
+
limit: Annotated[int, typer.Option("--limit", "-n", help="Max calls to return (newest first).")] = 20,
|
|
55
|
+
workspace: Annotated[str | None, typer.Option("--workspace", help="Workspace identifier override.")] = None,
|
|
56
|
+
base_url: Annotated[str | None, typer.Option("--base-url", help="API base URL override.")] = None,
|
|
57
|
+
output: Annotated[
|
|
58
|
+
str | None,
|
|
59
|
+
typer.Option("--output", "-o", help="Output format: table (default), json, id. Agent mode defaults to json."),
|
|
60
|
+
] = None,
|
|
61
|
+
):
|
|
62
|
+
"""List an agent's voice calls, newest first.
|
|
63
|
+
|
|
64
|
+
One row per Talk press: when, language, status, why it ended, how many
|
|
65
|
+
turns, how long. Read one call's turns with
|
|
66
|
+
``fruxon agents voice-calls get <agent> <voice-session-id>``.
|
|
67
|
+
|
|
68
|
+
A call with no turns and ``START_TIMEOUT`` never reached a voice worker;
|
|
69
|
+
``ENDED`` with zero turns and ``USER_HANGUP`` is a caller who hung up
|
|
70
|
+
before saying anything the worker heard.
|
|
71
|
+
|
|
72
|
+
Examples:
|
|
73
|
+
fruxon agents voice-calls list my-agent
|
|
74
|
+
fruxon agents voice-calls list my-agent -n 5 --output json
|
|
75
|
+
fruxon agents voice-calls list my-agent -o id | head -1
|
|
76
|
+
"""
|
|
77
|
+
output = resolve_output_format(output, human_default="table", agent_default="json")
|
|
78
|
+
if output not in {"table", "json", "id"}:
|
|
79
|
+
fail(
|
|
80
|
+
f"Unknown output format: [bold]{output}[/bold]",
|
|
81
|
+
hint="Valid formats: table, json, id.",
|
|
82
|
+
code=EXIT_VALIDATION,
|
|
83
|
+
)
|
|
84
|
+
if limit <= 0:
|
|
85
|
+
fail("--limit must be greater than zero.", code=EXIT_VALIDATION)
|
|
86
|
+
|
|
87
|
+
client = build_client(workspace, base_url)
|
|
88
|
+
try:
|
|
89
|
+
calls = client.list_voice_calls(agent, limit=limit)
|
|
90
|
+
except FruxonError as e:
|
|
91
|
+
fail_from_api_error(e, hint=_hint_for_get_error(e, agent, client))
|
|
92
|
+
|
|
93
|
+
if output == "id":
|
|
94
|
+
for c in calls:
|
|
95
|
+
print(c.id)
|
|
96
|
+
return
|
|
97
|
+
if output == "json":
|
|
98
|
+
print(json.dumps([_call_to_dict(c) for c in calls], indent=2, ensure_ascii=False))
|
|
99
|
+
return
|
|
100
|
+
|
|
101
|
+
if not calls:
|
|
102
|
+
say_info(
|
|
103
|
+
f"[bold]{agent}[/bold] has no voice calls.",
|
|
104
|
+
hint="Calls are made with the Talk button in the agent's Test lab.",
|
|
105
|
+
)
|
|
106
|
+
return
|
|
107
|
+
if stderr.is_terminal:
|
|
108
|
+
print_banner(subtitle=f"voice calls · {agent} · {len(calls)}")
|
|
109
|
+
stderr.print()
|
|
110
|
+
stderr.print(_build_calls_table(calls))
|
|
111
|
+
stderr.print()
|
|
112
|
+
say_ok(
|
|
113
|
+
f"[bold]{len(calls)}[/bold] call(s). Read one: "
|
|
114
|
+
f"[dim]fruxon agents voice-calls get {agent} <voice-session-id>[/dim]"
|
|
115
|
+
)
|
|
116
|
+
if len(calls) >= limit:
|
|
117
|
+
say_info(f"Showing the newest {limit}. Raise [bold]-n[/bold] to see older calls.")
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@voice_calls_app.command("get")
|
|
121
|
+
def voice_calls_get(
|
|
122
|
+
agent: Annotated[str, typer.Argument(help="Agent the call was made to.")],
|
|
123
|
+
voice_session: Annotated[str, typer.Argument(help="The call's id (from `voice-calls list`).")],
|
|
124
|
+
workspace: Annotated[str | None, typer.Option("--workspace", help="Workspace identifier override.")] = None,
|
|
125
|
+
base_url: Annotated[str | None, typer.Option("--base-url", help="API base URL override.")] = None,
|
|
126
|
+
output: Annotated[
|
|
127
|
+
str | None,
|
|
128
|
+
typer.Option(
|
|
129
|
+
"--output",
|
|
130
|
+
"-o",
|
|
131
|
+
help="Output format: text (default; the whole transcript), table (one row a turn), json.",
|
|
132
|
+
),
|
|
133
|
+
] = None,
|
|
134
|
+
):
|
|
135
|
+
"""Show one voice call turn by turn: what the caller said, the reply, what was heard.
|
|
136
|
+
|
|
137
|
+
**What was heard is not always the reply.** ``delivery`` is
|
|
138
|
+
``INTERRUPTED`` when the caller cut in; ``spokenText`` is then the part
|
|
139
|
+
they heard, and the rest of ``replyText`` never reached them.
|
|
140
|
+
``generation`` is separate: ``CANCELLED`` is a reply stopped by that
|
|
141
|
+
interruption or by the caller's next turn, ``PENDING`` a turn the caller
|
|
142
|
+
talked over before it ran.
|
|
143
|
+
|
|
144
|
+
Every turn names its run in ``executionRecordId``; follow it with
|
|
145
|
+
``fruxon agents executions trace <agent> <execution-id>`` to see the
|
|
146
|
+
tools, the prompt and the model call behind a reply. Times are epoch ms;
|
|
147
|
+
``userStartedAt`` / ``userEndedAt`` are the voice worker's clock.
|
|
148
|
+
|
|
149
|
+
Examples:
|
|
150
|
+
fruxon agents voice-calls get my-agent 0199f3c2-…
|
|
151
|
+
fruxon agents voice-calls get my-agent 0199f3c2-… --output table
|
|
152
|
+
fruxon agents voice-calls get my-agent 0199f3c2-… -o json | jq '.turns[] | select(.delivery == "INTERRUPTED")'
|
|
153
|
+
"""
|
|
154
|
+
output = resolve_output_format(output, human_default="text", agent_default="json")
|
|
155
|
+
if output not in {"text", "table", "json"}:
|
|
156
|
+
fail(
|
|
157
|
+
f"Unknown output format: [bold]{output}[/bold]",
|
|
158
|
+
hint="Valid formats: text, table, json.",
|
|
159
|
+
code=EXIT_VALIDATION,
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
client = build_client(workspace, base_url)
|
|
163
|
+
try:
|
|
164
|
+
call = client.get_voice_call(agent, voice_session)
|
|
165
|
+
except FruxonError as e:
|
|
166
|
+
fail_from_api_error(e, hint=_hint_for_call_error(e, agent, voice_session, client))
|
|
167
|
+
|
|
168
|
+
if output == "json":
|
|
169
|
+
print(json.dumps(_call_to_dict(call, with_turns=True), indent=2, ensure_ascii=False))
|
|
170
|
+
return
|
|
171
|
+
|
|
172
|
+
if stderr.is_terminal:
|
|
173
|
+
print_banner(subtitle=f"voice call · {agent} · {len(call.turns)} turn(s)")
|
|
174
|
+
stderr.print()
|
|
175
|
+
_render_call_header(call)
|
|
176
|
+
stderr.print()
|
|
177
|
+
if not call.turns:
|
|
178
|
+
say_info("No turns: the worker never heard the caller say anything.")
|
|
179
|
+
return
|
|
180
|
+
if output == "table":
|
|
181
|
+
stderr.print(_build_turns_table(call))
|
|
182
|
+
else:
|
|
183
|
+
for index, turn in enumerate(call.turns, start=1):
|
|
184
|
+
_render_turn(call, index, turn)
|
|
185
|
+
stderr.print()
|
|
186
|
+
if any(t.execution_record_id for t in call.turns):
|
|
187
|
+
say_ok(f"Trace a reply: [dim]fruxon agents executions trace {agent} <execution-id>[/dim]")
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
191
|
+
# helpers
|
|
192
|
+
# ─────────────────────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _hint_for_call_error(error: FruxonError, agent: str, voice_session: str, client: FruxonClient) -> str | None:
|
|
196
|
+
"""A 404 on ``get`` is usually the call id, not the agent; say which."""
|
|
197
|
+
if not isinstance(error, NotFoundError):
|
|
198
|
+
return _hint_for_get_error(error, agent, client)
|
|
199
|
+
try:
|
|
200
|
+
client.get_agent(agent)
|
|
201
|
+
except FruxonError:
|
|
202
|
+
return _hint_for_get_error(error, agent, client)
|
|
203
|
+
return (
|
|
204
|
+
f"[bold]{agent}[/bold] has no voice call [bold]{voice_session}[/bold]. "
|
|
205
|
+
f"List its calls with [bold]fruxon agents voice-calls list {agent}[/bold]."
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _call_length_ms(call: VoiceCall) -> int | None:
|
|
210
|
+
if call.call_duration_ms is not None:
|
|
211
|
+
return call.call_duration_ms
|
|
212
|
+
if call.started_at is not None and call.ended_at is not None:
|
|
213
|
+
return call.ended_at - call.started_at
|
|
214
|
+
return None
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _offset(call: VoiceCall, turn: VoiceCallTurn) -> str:
|
|
218
|
+
"""When the turn began, from the start of the call: the caller's first word if known."""
|
|
219
|
+
origin = call.started_at or call.created_at
|
|
220
|
+
at = turn.user_started_at or turn.created_at
|
|
221
|
+
if not origin or not at:
|
|
222
|
+
return ""
|
|
223
|
+
return f"+{format_duration(max(at - origin, 0))}"
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
_DELIVERY_COLOR = {"PLAYED": "green", "INTERRUPTED": "yellow", "UNKNOWN": "red"}
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _delivery_word(turn: VoiceCallTurn) -> str:
|
|
230
|
+
delivery = turn.delivery or "?"
|
|
231
|
+
color = _DELIVERY_COLOR.get(delivery)
|
|
232
|
+
return f"[{color}]{delivery}[/{color}]" if color else delivery
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _delivery_label(turn: VoiceCallTurn) -> str:
|
|
236
|
+
delivery = turn.delivery or "?"
|
|
237
|
+
label = _delivery_word(turn)
|
|
238
|
+
if turn.delivered_duration_ms is not None and delivery in {"PLAYED", "INTERRUPTED"}:
|
|
239
|
+
label += f" [dim]({format_duration(turn.delivered_duration_ms)} played)[/dim]"
|
|
240
|
+
if turn.generation and turn.generation != "DONE":
|
|
241
|
+
label += f" · generation {turn.generation}"
|
|
242
|
+
return label
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def _render_call_header(call: VoiceCall) -> None:
|
|
246
|
+
status = call.status or "?"
|
|
247
|
+
reason = f" · {call.end_reason}" if call.end_reason else ""
|
|
248
|
+
stderr.print(f" [bold]{call.id}[/bold] {status} · {call.language or '?'}{reason}")
|
|
249
|
+
length = _call_length_ms(call)
|
|
250
|
+
parts = [f"started {format_ts(call.started_at)}" if call.started_at else f"pressed {format_ts(call.created_at)}"]
|
|
251
|
+
if length is not None:
|
|
252
|
+
parts.append(format_duration(length))
|
|
253
|
+
parts.append(f"{call.turn_count or len(call.turns)} turn(s)")
|
|
254
|
+
stderr.print(f" [dim]{' · '.join(parts)}[/dim]")
|
|
255
|
+
stderr.print(f" [dim]Test lab session {call.fruxon_session_id}[/dim]")
|
|
256
|
+
speech = []
|
|
257
|
+
if call.stt_provider:
|
|
258
|
+
heard = f" ({call.stt_seconds:.1f}s)" if call.stt_seconds is not None else ""
|
|
259
|
+
speech.append(f"speech-to-text {call.stt_provider}{heard}")
|
|
260
|
+
if call.tts_provider:
|
|
261
|
+
chars = f" ({call.tts_characters} chars)" if call.tts_characters is not None else ""
|
|
262
|
+
speech.append(f"voice {call.tts_provider}{chars}")
|
|
263
|
+
if speech:
|
|
264
|
+
stderr.print(f" [dim]{' · '.join(speech)}[/dim]")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _render_turn(call: VoiceCall, index: int, turn: VoiceCallTurn) -> None:
|
|
268
|
+
stderr.print(f" [bold]#{index}[/bold] [dim]{_offset(call, turn)}[/dim] {_delivery_label(turn)}")
|
|
269
|
+
caller = markup_escape(turn.transcript) if turn.transcript else "[dim]— (no transcript)[/dim]"
|
|
270
|
+
stderr.print(f" [bold]Caller[/bold] {caller}")
|
|
271
|
+
reply = markup_escape(turn.reply_text) if turn.reply_text else "[dim]— (no reply)[/dim]"
|
|
272
|
+
stderr.print(f" [bold]Agent[/bold] {reply}")
|
|
273
|
+
if turn.delivery == "INTERRUPTED":
|
|
274
|
+
heard = markup_escape(turn.spoken_text) if turn.spoken_text else "[dim]nothing[/dim]"
|
|
275
|
+
stderr.print(f" [bold]Heard[/bold] {heard}")
|
|
276
|
+
if turn.execution_record_id:
|
|
277
|
+
stderr.print(f" [dim]execution {turn.execution_record_id}[/dim]")
|
|
278
|
+
stderr.print()
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _cell(text: str) -> str:
|
|
282
|
+
flat = " ".join(text.split())
|
|
283
|
+
return markup_escape(flat if len(flat) <= _CELL_CHARS else flat[: _CELL_CHARS - 1] + "…")
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
def _build_calls_table(calls: list[VoiceCall]):
|
|
287
|
+
from rich.table import Table
|
|
288
|
+
|
|
289
|
+
table = Table(show_header=True, header_style="bold", show_edge=False, pad_edge=False, box=None, padding=(0, 1))
|
|
290
|
+
table.add_column("ID", style="bold", no_wrap=True)
|
|
291
|
+
table.add_column("Pressed", no_wrap=True, style="dim")
|
|
292
|
+
table.add_column("Lang", no_wrap=True)
|
|
293
|
+
table.add_column("Status", no_wrap=True)
|
|
294
|
+
table.add_column("Ended", no_wrap=True)
|
|
295
|
+
table.add_column("Turns", justify="right")
|
|
296
|
+
table.add_column("Length", justify="right", no_wrap=True)
|
|
297
|
+
for c in calls:
|
|
298
|
+
length = _call_length_ms(c)
|
|
299
|
+
table.add_row(
|
|
300
|
+
c.id,
|
|
301
|
+
format_ts(c.created_at),
|
|
302
|
+
c.language or "[dim]—[/dim]",
|
|
303
|
+
c.status or "[dim]—[/dim]",
|
|
304
|
+
c.end_reason or "[dim]—[/dim]",
|
|
305
|
+
str(c.turn_count),
|
|
306
|
+
format_duration(length) if length is not None else "[dim]—[/dim]",
|
|
307
|
+
)
|
|
308
|
+
return table
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _build_turns_table(call: VoiceCall):
|
|
312
|
+
from rich.table import Table
|
|
313
|
+
|
|
314
|
+
table = Table(show_header=True, header_style="bold", show_edge=False, pad_edge=False, box=None, padding=(0, 1))
|
|
315
|
+
table.add_column("#", justify="right", style="bold")
|
|
316
|
+
table.add_column("At", no_wrap=True, style="dim")
|
|
317
|
+
table.add_column("Caller", overflow="fold", ratio=1, min_width=16)
|
|
318
|
+
table.add_column("Heard", overflow="fold", ratio=1, min_width=16)
|
|
319
|
+
table.add_column("Delivery", no_wrap=True)
|
|
320
|
+
table.add_column("Execution", no_wrap=True, style="dim")
|
|
321
|
+
for index, t in enumerate(call.turns, start=1):
|
|
322
|
+
# What the caller heard: all of the reply, or the part before they cut in.
|
|
323
|
+
heard = (t.spoken_text or "") if t.delivery == "INTERRUPTED" else t.reply_text
|
|
324
|
+
table.add_row(
|
|
325
|
+
str(index),
|
|
326
|
+
_offset(call, t),
|
|
327
|
+
_cell(t.transcript) if t.transcript else "[dim]—[/dim]",
|
|
328
|
+
_cell(heard) if heard else "[dim]—[/dim]",
|
|
329
|
+
_delivery_word(t),
|
|
330
|
+
t.execution_record_id or "[dim]—[/dim]",
|
|
331
|
+
)
|
|
332
|
+
return table
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def _turn_to_dict(t: VoiceCallTurn) -> dict:
|
|
336
|
+
return {
|
|
337
|
+
"id": t.id,
|
|
338
|
+
"turnId": t.turn_id,
|
|
339
|
+
"language": t.language,
|
|
340
|
+
"transcript": t.transcript,
|
|
341
|
+
"replyText": t.reply_text,
|
|
342
|
+
"generation": t.generation,
|
|
343
|
+
"delivery": t.delivery,
|
|
344
|
+
"spokenText": t.spoken_text,
|
|
345
|
+
"deliveredDurationMs": t.delivered_duration_ms,
|
|
346
|
+
"executionRecordId": t.execution_record_id or None,
|
|
347
|
+
"createdAt": t.created_at,
|
|
348
|
+
"userStartedAt": t.user_started_at,
|
|
349
|
+
"userEndedAt": t.user_ended_at,
|
|
350
|
+
"generationEndedAt": t.generation_ended_at,
|
|
351
|
+
"cancelRequestedAt": t.cancel_requested_at,
|
|
352
|
+
"deliveryReportedAt": t.delivery_reported_at,
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _call_to_dict(c: VoiceCall, *, with_turns: bool = False) -> dict:
|
|
357
|
+
body = {
|
|
358
|
+
"id": c.id,
|
|
359
|
+
"agentId": c.agent_id,
|
|
360
|
+
"fruxonSessionId": c.fruxon_session_id,
|
|
361
|
+
"status": c.status,
|
|
362
|
+
"language": c.language,
|
|
363
|
+
"createdAt": c.created_at,
|
|
364
|
+
"startedAt": c.started_at,
|
|
365
|
+
"endedAt": c.ended_at,
|
|
366
|
+
"endReason": c.end_reason or None,
|
|
367
|
+
"callDurationMs": c.call_duration_ms,
|
|
368
|
+
"turnCount": c.turn_count,
|
|
369
|
+
"createdByUserId": c.created_by_user_id or None,
|
|
370
|
+
}
|
|
371
|
+
if with_turns:
|
|
372
|
+
body |= {
|
|
373
|
+
"sttProvider": c.stt_provider or None,
|
|
374
|
+
"ttsProvider": c.tts_provider or None,
|
|
375
|
+
"sttSeconds": c.stt_seconds,
|
|
376
|
+
"ttsCharacters": c.tts_characters,
|
|
377
|
+
"turns": [_turn_to_dict(t) for t in c.turns],
|
|
378
|
+
}
|
|
379
|
+
return body
|
|
@@ -99,6 +99,7 @@ _EXAMPLES: dict[str, list[tuple[str, str]]] = {
|
|
|
99
99
|
),
|
|
100
100
|
("fruxon agents tests list my-agent", "List your saved test-chat sessions for an agent"),
|
|
101
101
|
("fruxon agents executions list my-agent", "List past executions — find a record id to trace"),
|
|
102
|
+
("fruxon agents voice-calls list my-agent", "Talk calls made to the agent — find one to read back"),
|
|
102
103
|
("fruxon agents budget list my-agent", "Every cost cap — PRODUCTION + TEST, daily / weekly / monthly"),
|
|
103
104
|
(
|
|
104
105
|
"fruxon agents budget set my-agent --origin TEST --period DAILY --amount 20",
|
|
@@ -373,6 +374,15 @@ _EXAMPLES: dict[str, list[tuple[str, str]]] = {
|
|
|
373
374
|
),
|
|
374
375
|
("fruxon agents inbox my-agent", "What the agent is focused on (per participant)"),
|
|
375
376
|
],
|
|
377
|
+
"voice-calls": [
|
|
378
|
+
("fruxon agents voice-calls list my-agent", "The agent's Talk calls, newest first — status, turns, length"),
|
|
379
|
+
("fruxon agents voice-calls get my-agent 0199f3c2-…", "One call turn by turn: said, replied, heard"),
|
|
380
|
+
("fruxon agents voice-calls get my-agent 0199f3c2-… -o table", "…one row a turn, with each turn's run"),
|
|
381
|
+
(
|
|
382
|
+
"fruxon agents voice-calls get my-agent 0199f3c2-… -o json | jq '.turns[].executionRecordId'",
|
|
383
|
+
"The runs behind the replies — feed one to `agents executions trace`",
|
|
384
|
+
),
|
|
385
|
+
],
|
|
376
386
|
"memory": [
|
|
377
387
|
("fruxon agents memory list my-agent --subject user_42", "What the agent remembers about one person"),
|
|
378
388
|
("fruxon agents memory list my-agent --query 'dark mode'", "Full-text over its memories"),
|
|
@@ -734,6 +744,11 @@ _EXAMPLES: dict[str, list[tuple[str, str]]] = {
|
|
|
734
744
|
("fruxon llm-providers configs get vertex_ai 4950…", "Draft (masked), published revision, health"),
|
|
735
745
|
("fruxon llm-providers configs test vertex_ai 4950…", "Probe every surface sharing the credential"),
|
|
736
746
|
("fruxon llm-providers configs test vertex_ai_maas 4950…", "…or just that one surface"),
|
|
747
|
+
(
|
|
748
|
+
"fruxon llm-providers configs complete google 4950… --model gemini-3.1-flash-lite "
|
|
749
|
+
"--system @voice.txt -m 'hi'",
|
|
750
|
+
"One completion on the config, no agent: does the model follow this system prompt?",
|
|
751
|
+
),
|
|
737
752
|
("fruxon llm-providers configs publish vertex_ai 4950…", "Ship the draft; every agent on it switches"),
|
|
738
753
|
(
|
|
739
754
|
"fruxon llm-providers connect openai",
|