fruxon 0.13.2__tar.gz → 0.13.3__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-0.13.2 → fruxon-0.13.3}/HISTORY.md +16 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/PKG-INFO +2 -1
- {fruxon-0.13.2 → fruxon-0.13.3}/README.md +1 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/_version.py +2 -2
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/__init__.py +1 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/agents_executions.py +1 -0
- fruxon-0.13.3/src/fruxon/cli/knowledge_bases.py +266 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/fruxon.py +70 -0
- fruxon-0.13.3/tests/test_knowledge_bases.py +278 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/.gitignore +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/LICENSE +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/pyproject.toml +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/__init__.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/__main__.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/_ssl.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/_crash.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/_schema.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/_shared.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/_stream.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/agents.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/agents_approvals.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/agents_budget.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/agents_channels.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/agents_draft.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/agents_lifecycle.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/agents_memory.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/agents_revisions.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/agents_sandbox.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/agents_sandbox_test.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/agents_slots.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/agents_tests.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/agents_topics.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/applications.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/applications_entry_points.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/assets.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/auth.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/capabilities.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/completion.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/config.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/consult_pins.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/describe.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/doctor.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/environments.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/escalations.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/examples.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/guides.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/integrations.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/keys.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/llm_providers.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/messages.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/metrics.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/participants.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/pipelines.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/secrets.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/skills.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/storage.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/tools.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/triggers.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/triggers_ledger.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/triggers_questions.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/triggers_shape.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/cli/workspaces.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/credentials.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/device_auth.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/doctor.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/exceptions.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/flow_validation.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/models.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/output.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/params.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/sandbox_scenario.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/skills/__init__.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/skills/fruxon-agent-mode/SKILL.md +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/skills/fruxon-build-agent/SKILL.md +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/skills/fruxon-create-integration/SKILL.md +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/skills/fruxon-debug-trace/SKILL.md +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/skills/fruxon-meet/SKILL.md +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/skills/fruxon-use-integrations/SKILL.md +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/telemetry.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/ui.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/update_check.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/src/fruxon/validation.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/__init__.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/conftest.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_actor.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_agent_lifecycle.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_agent_slots.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_agents_check_cli.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_agents_revisions_cli.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_agents_sandbox.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_applications.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_approvals.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_assets.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_budgets.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_capabilities.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_channels.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_cli.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_cli_base_url_routing.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_cli_crash.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_client.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_connect_nudge.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_consult.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_credentials.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_describe.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_device_auth.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_doctor.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_draft_bootstrap.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_draft_evaluate_cli.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_draft_pull_shape.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_draft_validate_cli.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_drafts.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_entry_points.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_environments.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_escalations.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_execution_records.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_flow_validation.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_fruxon.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_guides.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_integration_triggers_cli.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_integrations_authorize_cli.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_kb_trial_cli.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_llm_provider_configs_cli.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_memory.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_messages.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_metrics.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_online_secret_refs.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_output.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_params.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_participants.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_participants_write.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_pipelines.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_sandbox_scenario.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_schema.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_secrets.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_skills.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_ssl.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_storage.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_telemetry.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_test_chats.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_topics.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_triggers.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_triggers_ledger.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_triggers_questions.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_triggers_shape.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_triggers_write.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_ui.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_update_check.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_validation.py +0 -0
- {fruxon-0.13.2 → fruxon-0.13.3}/tests/test_workspaces_cli.py +0 -0
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# History
|
|
2
2
|
|
|
3
|
+
## Unreleased — A knowledge base can be consolidated
|
|
4
|
+
|
|
5
|
+
Serial merge-per-conversation cannot repair a pair once both halves exist:
|
|
6
|
+
each was written before the other was searchable. The backend now runs a
|
|
7
|
+
pass over the whole corpus (fruxon-backend, `knowledgeBases/{kb}:consolidate`).
|
|
8
|
+
|
|
9
|
+
- **New `fruxon knowledge-bases consolidate <kb>`** — embeds the base's
|
|
10
|
+
live documents, clusters those above a similarity threshold, and prints
|
|
11
|
+
the clusters. Report-only unless `--apply`, which has the base's
|
|
12
|
+
consolidator agent decide each cluster and merge it (survivor keeps the
|
|
13
|
+
union of tags, labels and source conversations; the rest are archived).
|
|
14
|
+
Waits for the report by default; `--no-wait` prints the queued run and
|
|
15
|
+
`fruxon knowledge-bases consolidation <kb> <run>` reads it later.
|
|
16
|
+
- `agents executions list --trigger-type` accepts `KNOWLEDGE_CONSOLIDATION`,
|
|
17
|
+
the trigger the consolidator agent's runs carry.
|
|
18
|
+
|
|
3
19
|
## Unreleased — What a listing leaves out, and what a typo returns
|
|
4
20
|
|
|
5
21
|
A patch release from a CLI trial run against a live workspace. Four defects,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fruxon
|
|
3
|
-
Version: 0.13.
|
|
3
|
+
Version: 0.13.3
|
|
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
|
|
@@ -138,6 +138,7 @@ Integrations + tools:
|
|
|
138
138
|
| `fruxon keys list/mint/revoke/delete/history/scopes` | Audit and revoke scoped tokens. Minting opens the dashboard so the secret never enters the CLI process. |
|
|
139
139
|
| `fruxon llm-providers list/get/models` | Browse LLM providers and models supported at the tenant level. |
|
|
140
140
|
| `fruxon assets create/list/get/wait/operations/delete` | Manage knowledge-base (RAG) assets a step 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. |
|
|
141
|
+
| `fruxon knowledge-bases consolidate/consolidation` | Find the near-duplicate articles a per-conversation merge left in an editable knowledge base and, with `--apply`, have the base's consolidator agent fold each cluster into one (union of tags, labels and sources; the rest archived with a `merged_into` label). Report-only by default; waits for the report unless `--no-wait`. |
|
|
141
142
|
| `fruxon assets search/documents/chunks` | Reproduce and inspect a retrieval. `search` runs the same query an agent's `search_assets` does (`--mode HYBRID/VECTOR/FULL_TEXT`, `--top-k`, fusion weights) — note `score` is on a different scale per mode, and in hybrid the top hit is always 1.0. `documents` lists what the index contains (a `chunkCount` of 0 means that file yielded no indexable text); `chunks` reads one document's chunks in order. The `documentId` these print is the **index's** id, which is what `chunks` takes — on a knowledge base's backing asset it is not the editable document's id, and it changes whenever a document's body is re-indexed. The `fileName` (`<knowledge document id>.md`) is the document a `knowledge_base` tool would edit. |
|
|
142
143
|
| `fruxon metrics list` | Browse the evaluation-metric catalog — the ids a step's LLM-judge config (`judge.metrics[]`) binds, with a default weight each. |
|
|
143
144
|
| `fruxon triggers list/get/create/update/delete/fire/bind/unbind` | Manage triggers — the schedule/event sources that fire agents. `create`/`update` take `--file` (+`--schema`); `create` also takes `--application`, the Application that will own the trigger and must own everything it fires. `bind`/`unbind` wire which agents fire; `fire` runs it now. `fire`/`delete`/`unbind` confirm + refuse without `--yes` in agent mode. The control plane for autonomy. `get` returns the full stored `workShape` and every `scheduleTimes` slot, so `get -o json | jq .workShape` produces a body `update --file` can post straight back — a PATCH replaces the whole shape, so edit what you read. |
|
|
@@ -105,6 +105,7 @@ Integrations + tools:
|
|
|
105
105
|
| `fruxon keys list/mint/revoke/delete/history/scopes` | Audit and revoke scoped tokens. Minting opens the dashboard so the secret never enters the CLI process. |
|
|
106
106
|
| `fruxon llm-providers list/get/models` | Browse LLM providers and models supported at the tenant level. |
|
|
107
107
|
| `fruxon assets create/list/get/wait/operations/delete` | Manage knowledge-base (RAG) assets a step 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. |
|
|
108
|
+
| `fruxon knowledge-bases consolidate/consolidation` | Find the near-duplicate articles a per-conversation merge left in an editable knowledge base and, with `--apply`, have the base's consolidator agent fold each cluster into one (union of tags, labels and sources; the rest archived with a `merged_into` label). Report-only by default; waits for the report unless `--no-wait`. |
|
|
108
109
|
| `fruxon assets search/documents/chunks` | Reproduce and inspect a retrieval. `search` runs the same query an agent's `search_assets` does (`--mode HYBRID/VECTOR/FULL_TEXT`, `--top-k`, fusion weights) — note `score` is on a different scale per mode, and in hybrid the top hit is always 1.0. `documents` lists what the index contains (a `chunkCount` of 0 means that file yielded no indexable text); `chunks` reads one document's chunks in order. The `documentId` these print is the **index's** id, which is what `chunks` takes — on a knowledge base's backing asset it is not the editable document's id, and it changes whenever a document's body is re-indexed. The `fileName` (`<knowledge document id>.md`) is the document a `knowledge_base` tool would edit. |
|
|
109
110
|
| `fruxon metrics list` | Browse the evaluation-metric catalog — the ids a step's LLM-judge config (`judge.metrics[]`) binds, with a default weight each. |
|
|
110
111
|
| `fruxon triggers list/get/create/update/delete/fire/bind/unbind` | Manage triggers — the schedule/event sources that fire agents. `create`/`update` take `--file` (+`--schema`); `create` also takes `--application`, the Application that will own the trigger and must own everything it fires. `bind`/`unbind` wire which agents fire; `fire` runs it now. `fire`/`delete`/`unbind` confirm + refuse without `--yes` in agent mode. The control plane for autonomy. `get` returns the full stored `workShape` and every `scheduleTimes` slot, so `get -o json | jq .workShape` produces a body `update --file` can post straight back — a PATCH replaces the whole shape, so edit what you read. |
|
|
@@ -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 = '0.13.
|
|
22
|
-
__version_tuple__ = version_tuple = (0, 13,
|
|
21
|
+
__version__ = version = '0.13.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (0, 13, 3)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -599,6 +599,7 @@ from fruxon.cli import examples as _examples # noqa: E402, F401
|
|
|
599
599
|
from fruxon.cli import guides as _guides # noqa: E402, F401
|
|
600
600
|
from fruxon.cli import integrations as _integrations # noqa: E402, F401
|
|
601
601
|
from fruxon.cli import keys as _keys # noqa: E402, F401
|
|
602
|
+
from fruxon.cli import knowledge_bases as _knowledge_bases # noqa: E402, F401
|
|
602
603
|
from fruxon.cli import llm_providers as _llm_providers # noqa: E402, F401
|
|
603
604
|
from fruxon.cli import messages as _messages # noqa: E402, F401
|
|
604
605
|
from fruxon.cli import metrics as _metrics # noqa: E402, F401
|
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
"""``fruxon knowledge-bases`` — passes over an editable knowledge base.
|
|
2
|
+
|
|
3
|
+
A *knowledge base* is the authored, reviewable corpus behind a
|
|
4
|
+
``manual_knowledge`` asset; its documents reach search only when the base
|
|
5
|
+
is published. This group carries the corpus-level operations that no
|
|
6
|
+
single document verb can express. Today that is ``consolidate``: find the
|
|
7
|
+
near-duplicate articles a per-item merge stage left behind and, on
|
|
8
|
+
``--apply``, have the base's consolidator agent fold each cluster into one.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import json
|
|
14
|
+
from typing import Annotated
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
|
|
18
|
+
from fruxon.cli import app
|
|
19
|
+
from fruxon.cli._shared import build_client
|
|
20
|
+
from fruxon.exceptions import FruxonError
|
|
21
|
+
from fruxon.ui import (
|
|
22
|
+
EXIT_VALIDATION,
|
|
23
|
+
fail,
|
|
24
|
+
fail_from_api_error,
|
|
25
|
+
resolve_output_format,
|
|
26
|
+
say_info,
|
|
27
|
+
say_ok,
|
|
28
|
+
say_warn,
|
|
29
|
+
stderr,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
knowledge_bases_app = typer.Typer(
|
|
33
|
+
help="Passes over an editable knowledge base (consolidate near-duplicate articles).",
|
|
34
|
+
no_args_is_help=True,
|
|
35
|
+
)
|
|
36
|
+
app.add_typer(knowledge_bases_app, name="knowledge-bases")
|
|
37
|
+
|
|
38
|
+
_TERMINAL = {"COMPLETED", "FAILED"}
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@knowledge_bases_app.command("consolidate")
|
|
42
|
+
def knowledge_bases_consolidate(
|
|
43
|
+
knowledge_base: Annotated[str, typer.Argument(help="Knowledge base id.")],
|
|
44
|
+
apply: Annotated[
|
|
45
|
+
bool,
|
|
46
|
+
typer.Option(
|
|
47
|
+
"--apply",
|
|
48
|
+
help=(
|
|
49
|
+
"Merge the clusters instead of only reporting them. Needs a consolidator agent on the "
|
|
50
|
+
"knowledge base; without one the pass reports whatever this says."
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
] = False,
|
|
54
|
+
threshold: Annotated[
|
|
55
|
+
float | None,
|
|
56
|
+
typer.Option(
|
|
57
|
+
"--threshold", help="Cosine similarity at or above which two documents cluster (server default 0.86)."
|
|
58
|
+
),
|
|
59
|
+
] = None,
|
|
60
|
+
max_cluster_size: Annotated[
|
|
61
|
+
int | None,
|
|
62
|
+
typer.Option(
|
|
63
|
+
"--max-cluster-size", help="Largest cluster handed to the agent in one decision (server default 5)."
|
|
64
|
+
),
|
|
65
|
+
] = None,
|
|
66
|
+
recent_minutes: Annotated[
|
|
67
|
+
int | None,
|
|
68
|
+
typer.Option(
|
|
69
|
+
"--recent-minutes", help="Leave out documents modified within this many minutes (server default 10)."
|
|
70
|
+
),
|
|
71
|
+
] = None,
|
|
72
|
+
wait: Annotated[
|
|
73
|
+
bool,
|
|
74
|
+
typer.Option("--wait/--no-wait", help="Block until the run completes and print its report (default: wait)."),
|
|
75
|
+
] = True,
|
|
76
|
+
timeout: Annotated[float, typer.Option("--timeout", help="Seconds before giving up on --wait.")] = 600.0,
|
|
77
|
+
poll_interval: Annotated[float, typer.Option("--poll-interval", help="Seconds between polls.")] = 3.0,
|
|
78
|
+
workspace: Annotated[str | None, typer.Option("--workspace", help="Workspace identifier override.")] = None,
|
|
79
|
+
base_url: Annotated[str | None, typer.Option("--base-url", help="API base URL override.")] = None,
|
|
80
|
+
output: Annotated[
|
|
81
|
+
str | None,
|
|
82
|
+
typer.Option("--output", "-o", help="Output format: text (default for humans), json (default in agent mode)."),
|
|
83
|
+
] = None,
|
|
84
|
+
):
|
|
85
|
+
"""Queue a consolidation pass and, by default, wait for its report.
|
|
86
|
+
|
|
87
|
+
Report-only unless ``--apply`` is given: the pass embeds the base's
|
|
88
|
+
Draft and Published documents, clusters the ones above the threshold,
|
|
89
|
+
and lists each cluster. With ``--apply`` and a consolidator agent set
|
|
90
|
+
on the knowledge base, the agent decides each cluster and a merge
|
|
91
|
+
writes one article (union of tags, labels and source conversations)
|
|
92
|
+
and archives the rest. The pass runs on the worker; ``--no-wait``
|
|
93
|
+
prints the queued run and returns.
|
|
94
|
+
|
|
95
|
+
Examples:
|
|
96
|
+
fruxon knowledge-bases consolidate <kb>
|
|
97
|
+
fruxon knowledge-bases consolidate <kb> --apply
|
|
98
|
+
fruxon knowledge-bases consolidate <kb> --threshold 0.9 --no-wait -o json
|
|
99
|
+
"""
|
|
100
|
+
output = resolve_output_format(output, human_default="text", agent_default="json")
|
|
101
|
+
if output not in {"text", "json"}:
|
|
102
|
+
fail(f"Unknown output format: [bold]{output}[/bold]", hint="Valid formats: text, json.", code=EXIT_VALIDATION)
|
|
103
|
+
if threshold is not None and not 0.5 <= threshold <= 1.0:
|
|
104
|
+
fail("--threshold must be between 0.5 and 1.0.", code=EXIT_VALIDATION)
|
|
105
|
+
if max_cluster_size is not None and max_cluster_size < 2:
|
|
106
|
+
fail("--max-cluster-size must be at least 2.", code=EXIT_VALIDATION)
|
|
107
|
+
if recent_minutes is not None and recent_minutes < 0:
|
|
108
|
+
fail("--recent-minutes cannot be negative.", code=EXIT_VALIDATION)
|
|
109
|
+
if timeout <= 0 or poll_interval <= 0:
|
|
110
|
+
fail("Timeout and poll interval must be positive.", code=EXIT_VALIDATION)
|
|
111
|
+
|
|
112
|
+
client = build_client(workspace, base_url)
|
|
113
|
+
try:
|
|
114
|
+
run = client.consolidate_knowledge_base(
|
|
115
|
+
knowledge_base,
|
|
116
|
+
report_only=not apply,
|
|
117
|
+
similarity_threshold=threshold,
|
|
118
|
+
max_cluster_size=max_cluster_size,
|
|
119
|
+
recent_write_window_minutes=recent_minutes,
|
|
120
|
+
)
|
|
121
|
+
except FruxonError as e:
|
|
122
|
+
fail_from_api_error(e, hint=_hint_for_error(e))
|
|
123
|
+
|
|
124
|
+
run_id = str(run.get("id", ""))
|
|
125
|
+
if not wait:
|
|
126
|
+
if output == "json":
|
|
127
|
+
print(json.dumps(run, indent=2))
|
|
128
|
+
return
|
|
129
|
+
say_ok(
|
|
130
|
+
f"Queued consolidation run [bold]{run_id}[/bold] on knowledge base [bold]{knowledge_base}[/bold].",
|
|
131
|
+
hint=f"Read it with: fruxon knowledge-bases consolidation {knowledge_base} {run_id}",
|
|
132
|
+
)
|
|
133
|
+
return
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
if stderr.is_terminal and output == "text":
|
|
137
|
+
with stderr.status(f"[bold]Consolidating [cyan]{knowledge_base}[/cyan]…[/bold]"):
|
|
138
|
+
run = client.wait_for_knowledge_base_consolidation(
|
|
139
|
+
knowledge_base, run_id, timeout_s=timeout, poll_interval_s=poll_interval
|
|
140
|
+
)
|
|
141
|
+
else:
|
|
142
|
+
run = client.wait_for_knowledge_base_consolidation(
|
|
143
|
+
knowledge_base, run_id, timeout_s=timeout, poll_interval_s=poll_interval
|
|
144
|
+
)
|
|
145
|
+
except TimeoutError as e:
|
|
146
|
+
fail(str(e), hint=f"Read it later with: fruxon knowledge-bases consolidation {knowledge_base} {run_id}")
|
|
147
|
+
except FruxonError as e:
|
|
148
|
+
fail_from_api_error(e, hint=_hint_for_error(e))
|
|
149
|
+
|
|
150
|
+
_emit_run(run, output)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@knowledge_bases_app.command("consolidation")
|
|
154
|
+
def knowledge_bases_consolidation(
|
|
155
|
+
knowledge_base: Annotated[str, typer.Argument(help="Knowledge base id.")],
|
|
156
|
+
run: Annotated[str, typer.Argument(help="Consolidation run id, as printed by `consolidate`.")],
|
|
157
|
+
workspace: Annotated[str | None, typer.Option("--workspace", help="Workspace identifier override.")] = None,
|
|
158
|
+
base_url: Annotated[str | None, typer.Option("--base-url", help="API base URL override.")] = None,
|
|
159
|
+
output: Annotated[
|
|
160
|
+
str | None,
|
|
161
|
+
typer.Option("--output", "-o", help="Output format: text (default for humans), json (default in agent mode)."),
|
|
162
|
+
] = None,
|
|
163
|
+
):
|
|
164
|
+
"""Read one consolidation run — its status, and the report once it completed."""
|
|
165
|
+
output = resolve_output_format(output, human_default="text", agent_default="json")
|
|
166
|
+
if output not in {"text", "json"}:
|
|
167
|
+
fail(f"Unknown output format: [bold]{output}[/bold]", hint="Valid formats: text, json.", code=EXIT_VALIDATION)
|
|
168
|
+
|
|
169
|
+
client = build_client(workspace, base_url)
|
|
170
|
+
try:
|
|
171
|
+
current = client.get_knowledge_base_consolidation(knowledge_base, run)
|
|
172
|
+
except FruxonError as e:
|
|
173
|
+
fail_from_api_error(e, hint=_hint_for_error(e))
|
|
174
|
+
|
|
175
|
+
_emit_run(current, output)
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _emit_run(run: dict, output: str) -> None:
|
|
179
|
+
if output == "json":
|
|
180
|
+
print(json.dumps(run, indent=2))
|
|
181
|
+
return
|
|
182
|
+
|
|
183
|
+
status = str(run.get("status", ""))
|
|
184
|
+
run_id = run.get("id", "")
|
|
185
|
+
if status == "FAILED":
|
|
186
|
+
fail(f"Consolidation run [bold]{run_id}[/bold] failed: {run.get('error') or 'no error recorded'}")
|
|
187
|
+
if status not in _TERMINAL:
|
|
188
|
+
say_info(f"Consolidation run [bold]{run_id}[/bold] is {status.lower() or 'pending'}; no report yet.")
|
|
189
|
+
return
|
|
190
|
+
|
|
191
|
+
report = run.get("report") or {}
|
|
192
|
+
_print_report(report, run)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def _print_report(report: dict, run: dict) -> None:
|
|
196
|
+
from rich.markup import escape
|
|
197
|
+
from rich.table import Table
|
|
198
|
+
|
|
199
|
+
mode = "report only" if report.get("reportOnly", True) else "applied"
|
|
200
|
+
agent = report.get("consolidatorAgentId")
|
|
201
|
+
say_ok(
|
|
202
|
+
f"Consolidation run [bold]{run.get('id', '')}[/bold] completed ({mode}"
|
|
203
|
+
+ (f", agent [bold]{escape(str(agent))}[/bold]" if agent else ", no consolidator agent")
|
|
204
|
+
+ ")."
|
|
205
|
+
)
|
|
206
|
+
stderr.print(
|
|
207
|
+
f" documents {report.get('documentsConsidered', 0)}"
|
|
208
|
+
f" · skipped as recently modified {report.get('documentsSkippedAsRecentlyModified', 0)}"
|
|
209
|
+
f" · embeddings computed {report.get('embeddingsComputed', 0)}"
|
|
210
|
+
f" / reused {report.get('embeddingsReused', 0)}"
|
|
211
|
+
+ (f" · model {escape(str(report['embeddingModel']))}" if report.get("embeddingModel") else "")
|
|
212
|
+
)
|
|
213
|
+
stderr.print(
|
|
214
|
+
f" clusters {report.get('clustersFound', 0)}"
|
|
215
|
+
f" · merged {report.get('clustersMerged', 0)}"
|
|
216
|
+
f" · kept separate {report.get('clustersKeptSeparate', 0)}"
|
|
217
|
+
f" · failed {report.get('clustersFailed', 0)}"
|
|
218
|
+
f" · documents archived {report.get('documentsArchived', 0)}"
|
|
219
|
+
f" · threshold {report.get('similarityThreshold', '')}"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
clusters = report.get("clusters") or []
|
|
223
|
+
if not clusters:
|
|
224
|
+
say_info("No clusters above the threshold — nothing to consolidate.")
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
for cluster in clusters:
|
|
228
|
+
outcome = str(cluster.get("outcome", ""))
|
|
229
|
+
header = (
|
|
230
|
+
f"[bold]#{cluster.get('index', '')}[/bold] {outcome.lower().replace('_', ' ')}"
|
|
231
|
+
f" · similarity {cluster.get('minSimilarity', '')}–{cluster.get('maxSimilarity', '')}"
|
|
232
|
+
)
|
|
233
|
+
if cluster.get("survivorId"):
|
|
234
|
+
header += f" · survivor {cluster['survivorId']}"
|
|
235
|
+
stderr.print(header)
|
|
236
|
+
table = Table(show_header=True, header_style="bold", show_edge=False, pad_edge=False, box=None, padding=(0, 1))
|
|
237
|
+
table.add_column("document")
|
|
238
|
+
table.add_column("status")
|
|
239
|
+
table.add_column("sources", justify="right")
|
|
240
|
+
table.add_column("title")
|
|
241
|
+
archived = set(cluster.get("archivedIds") or [])
|
|
242
|
+
for member in cluster.get("members") or []:
|
|
243
|
+
member_id = str(member.get("id", ""))
|
|
244
|
+
status = str(member.get("status", ""))
|
|
245
|
+
if member_id in archived:
|
|
246
|
+
status = "ARCHIVED (merged)"
|
|
247
|
+
table.add_row(
|
|
248
|
+
member_id, status, str(member.get("sourceConversationCount", 0)), escape(str(member.get("title", "")))
|
|
249
|
+
)
|
|
250
|
+
stderr.print(table)
|
|
251
|
+
if cluster.get("note"):
|
|
252
|
+
stderr.print(f" note: {escape(str(cluster['note']))}")
|
|
253
|
+
if report.get("clustersFailed", 0):
|
|
254
|
+
say_warn("Some clusters failed; their notes say why. Nothing was written for those.")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _hint_for_error(e: FruxonError) -> str | None:
|
|
258
|
+
status = getattr(e, "status", None)
|
|
259
|
+
if status == 404:
|
|
260
|
+
return (
|
|
261
|
+
"Check the knowledge base id: fruxon assets list shows its backing asset, "
|
|
262
|
+
"the id here is the knowledge base's own."
|
|
263
|
+
)
|
|
264
|
+
if status == 409:
|
|
265
|
+
return "A pass is already queued or running on this knowledge base, or the base is archived."
|
|
266
|
+
return None
|
|
@@ -2437,6 +2437,76 @@ class FruxonClient:
|
|
|
2437
2437
|
raw = self._get_json(self._assets_url(f"/{asset}/operations/{operation}"))
|
|
2438
2438
|
return raw if isinstance(raw, dict) else {}
|
|
2439
2439
|
|
|
2440
|
+
# ── Knowledge bases ──────────────────────────────────────────────────
|
|
2441
|
+
|
|
2442
|
+
def _knowledge_bases_url(self, action: str = "") -> str:
|
|
2443
|
+
return f"{self._base_url}/v1/tenants/{self._workspace}/knowledgeBases{action}"
|
|
2444
|
+
|
|
2445
|
+
def consolidate_knowledge_base(
|
|
2446
|
+
self,
|
|
2447
|
+
knowledge_base: str,
|
|
2448
|
+
*,
|
|
2449
|
+
report_only: bool = True,
|
|
2450
|
+
similarity_threshold: float | None = None,
|
|
2451
|
+
max_cluster_size: int | None = None,
|
|
2452
|
+
recent_write_window_minutes: int | None = None,
|
|
2453
|
+
) -> dict:
|
|
2454
|
+
"""Queue a consolidation pass over a knowledge base (``POST …:consolidate``).
|
|
2455
|
+
|
|
2456
|
+
The pass runs on the maintenance worker: the reply is the queued run,
|
|
2457
|
+
not the report. Poll :meth:`get_knowledge_base_consolidation` or use
|
|
2458
|
+
:meth:`wait_for_knowledge_base_consolidation`. ``report_only`` defaults
|
|
2459
|
+
to True, matching the server — a pass merges nothing unless told to.
|
|
2460
|
+
Omitted options fall back to the server's defaults, which the run
|
|
2461
|
+
records.
|
|
2462
|
+
"""
|
|
2463
|
+
body: dict[str, object] = {"reportOnly": report_only}
|
|
2464
|
+
if similarity_threshold is not None:
|
|
2465
|
+
body["similarityThreshold"] = similarity_threshold
|
|
2466
|
+
if max_cluster_size is not None:
|
|
2467
|
+
body["maxClusterSize"] = max_cluster_size
|
|
2468
|
+
if recent_write_window_minutes is not None:
|
|
2469
|
+
body["recentWriteWindowMinutes"] = recent_write_window_minutes
|
|
2470
|
+
return self._post_json(self._knowledge_bases_url(f"/{knowledge_base}:consolidate"), body)
|
|
2471
|
+
|
|
2472
|
+
def get_knowledge_base_consolidation(self, knowledge_base: str, run: str) -> dict:
|
|
2473
|
+
"""Fetch one consolidation run; ``report`` is present once it completed."""
|
|
2474
|
+
raw = self._get_json(self._knowledge_bases_url(f"/{knowledge_base}/consolidations/{run}"))
|
|
2475
|
+
return raw if isinstance(raw, dict) else {}
|
|
2476
|
+
|
|
2477
|
+
def list_knowledge_base_consolidations(self, knowledge_base: str, *, all_pages: bool = True) -> list[dict]:
|
|
2478
|
+
"""List consolidation runs over a knowledge base, newest first."""
|
|
2479
|
+
base_url = self._knowledge_bases_url(f"/{knowledge_base}/consolidations")
|
|
2480
|
+
items = self._fetch_all_pages(base_url, []) if all_pages else self._fetch_page(base_url, [], page_token=None)[0]
|
|
2481
|
+
return [item for item in items if isinstance(item, dict)]
|
|
2482
|
+
|
|
2483
|
+
def wait_for_knowledge_base_consolidation(
|
|
2484
|
+
self,
|
|
2485
|
+
knowledge_base: str,
|
|
2486
|
+
run: str,
|
|
2487
|
+
*,
|
|
2488
|
+
timeout_s: float = 600.0,
|
|
2489
|
+
poll_interval_s: float = 3.0,
|
|
2490
|
+
) -> dict:
|
|
2491
|
+
"""Poll a consolidation run until it is COMPLETED or FAILED.
|
|
2492
|
+
|
|
2493
|
+
Returns the terminal run (its ``report`` when completed, its ``error``
|
|
2494
|
+
when failed). Raises :class:`TimeoutError` past ``timeout_s``.
|
|
2495
|
+
"""
|
|
2496
|
+
deadline = time.monotonic() + timeout_s
|
|
2497
|
+
last_status = None
|
|
2498
|
+
while True:
|
|
2499
|
+
current = self.get_knowledge_base_consolidation(knowledge_base, run)
|
|
2500
|
+
last_status = current.get("status")
|
|
2501
|
+
if last_status in {"COMPLETED", "FAILED"}:
|
|
2502
|
+
return current
|
|
2503
|
+
if time.monotonic() >= deadline:
|
|
2504
|
+
raise TimeoutError(
|
|
2505
|
+
f"Timed out waiting for consolidation run {run} on knowledge base {knowledge_base}."
|
|
2506
|
+
f" Last status: {last_status}."
|
|
2507
|
+
)
|
|
2508
|
+
time.sleep(poll_interval_s)
|
|
2509
|
+
|
|
2440
2510
|
def list_asset_embedding_models(self) -> list[dict]:
|
|
2441
2511
|
"""List embedding models accepted by asset ingestion."""
|
|
2442
2512
|
raw = self._get_json(self._assets_url("/embeddingModels"))
|