unique-sdk 2026.32.0.dev8__tar.gz → 2026.32.0.dev10__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.
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/PKG-INFO +1 -1
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/pyproject.toml +1 -1
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/__init__.py +0 -3
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/cli.py +25 -107
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/commands/elicitation.py +160 -25
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/shell.py +9 -9
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/skills/unique-cli-file-management/SKILL.md +25 -18
- unique_sdk-2026.32.0.dev8/unique_sdk/api_resources/_dynamic_frontend.py +0 -101
- unique_sdk-2026.32.0.dev8/unique_sdk/cli/commands/dynamic_frontend.py +0 -155
- unique_sdk-2026.32.0.dev8/unique_sdk/cli/skills/unique-cli-dynamic-frontend/SKILL.md +0 -151
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/README.md +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/_api_requestor.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/_api_resource.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/_api_version.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/_error.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/_http_client.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/_list_object.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/_object_classes.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/_request_options.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/_unique_object.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/_unique_ql.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/_unique_response.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/_util.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/_version.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/_webhook.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/__init__.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_acronyms.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_agentic_table.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_analytics_order.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_benchmarking.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_briefing.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_chat_completion.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_content.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_elicitation.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_embedding.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_event.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_folder.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_group.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_integrated.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_llm_models.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_mcp.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_message.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_message_assessment.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_message_execution.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_message_log.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_message_tool.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_module.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_scheduled_task.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_search.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_search_string.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_short_term_memory.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_space.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_user.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/api_resources/_web_search.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/__init__.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/__main__.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/commands/__init__.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/commands/_citation_manifest.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/commands/agentic_table.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/commands/browser.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/commands/cite_file.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/commands/files.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/commands/folders.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/commands/mcp.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/commands/navigation.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/commands/read.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/commands/scheduled_tasks.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/commands/search.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/commands/subagent.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/commands/web_search.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/commands/web_search_config.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/config.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/formatting.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/identity.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/metadata_filter.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/skills/unique-cli-agentic-table/SKILL.md +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/skills/unique-cli-elicitation/SKILL.md +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/skills/unique-cli-mcp/SKILL.md +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/skills/unique-cli-scheduled-tasks/SKILL.md +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/skills/unique-cli-search/SKILL.md +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/skills/unique-cli-subagent/SKILL.md +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/skills/unique-cli-uploaded-search/SKILL.md +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/skills/unique-cli-web-search/SKILL.md +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/state.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/utils/analytics_order_run.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/utils/benchmarking_run.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/utils/chat_history.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/utils/chat_in_space.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/utils/file_io.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/utils/sources.py +0 -0
- {unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/utils/token.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: unique-sdk
|
|
3
|
-
Version: 2026.32.0.
|
|
3
|
+
Version: 2026.32.0.dev10
|
|
4
4
|
Summary:
|
|
5
5
|
Author: Martin Fadler, Konstantin Krauss, Andreas Hauri
|
|
6
6
|
Author-email: Martin Fadler <martin.fadler@unique.ch>, Konstantin Krauss <konstantin@unique.ch>, Andreas Hauri <andreas@unique.ch>
|
|
@@ -77,9 +77,6 @@ from unique_sdk.api_resources._message import Message as Message
|
|
|
77
77
|
from unique_sdk.api_resources._integrated import Integrated as Integrated
|
|
78
78
|
from unique_sdk.api_resources._search import Search as Search
|
|
79
79
|
from unique_sdk.api_resources._content import Content as Content
|
|
80
|
-
from unique_sdk.api_resources._dynamic_frontend import (
|
|
81
|
-
DynamicFrontend as DynamicFrontend,
|
|
82
|
-
)
|
|
83
80
|
from unique_sdk.api_resources._search_string import SearchString as SearchString
|
|
84
81
|
from unique_sdk.api_resources._short_term_memory import (
|
|
85
82
|
ShortTermMemory as ShortTermMemory,
|
|
@@ -31,11 +31,6 @@ from unique_sdk.cli.commands.cite_file import cmd_cite_file
|
|
|
31
31
|
from unique_sdk.cli.commands.cite_file import (
|
|
32
32
|
is_error_output as _is_cite_error_output,
|
|
33
33
|
)
|
|
34
|
-
from unique_sdk.cli.commands.dynamic_frontend import (
|
|
35
|
-
cmd_dynamic_frontend_delete,
|
|
36
|
-
cmd_dynamic_frontend_deploy,
|
|
37
|
-
cmd_dynamic_frontend_list,
|
|
38
|
-
)
|
|
39
34
|
from unique_sdk.cli.commands.elicitation import (
|
|
40
35
|
DEFAULT_WAIT_TIMEOUT_SECONDS,
|
|
41
36
|
cmd_elicit_ask,
|
|
@@ -97,8 +92,6 @@ from unique_sdk.cli.identity import TurnIdentityError, resolve_message_id
|
|
|
97
92
|
from unique_sdk.cli.shell import UniqueShell
|
|
98
93
|
from unique_sdk.cli.state import ShellState
|
|
99
94
|
|
|
100
|
-
_DYNAMIC_FRONTEND_ERROR_PREFIX = "dynamic-frontend "
|
|
101
|
-
|
|
102
95
|
|
|
103
96
|
def _resolve_cli_message_id(
|
|
104
97
|
ctx: click.Context,
|
|
@@ -174,7 +167,6 @@ Examples:
|
|
|
174
167
|
unique-cli subagent Legal "Review" Invoke a connected space/subagent
|
|
175
168
|
unique-cli web-search search "x" Search the web via the public API
|
|
176
169
|
unique-cli web-search crawl URL Crawl a URL via the public API
|
|
177
|
-
unique-cli dynamic-frontend list List manageable Dynamic Frontend spaces
|
|
178
170
|
unique-cli browser get-dom Read the user's live Chrome tab (a11y tree)
|
|
179
171
|
unique-cli agentic-table get-sheet mt_abc123 Show a magic-table sheet summary
|
|
180
172
|
"""
|
|
@@ -582,101 +574,6 @@ def read_cmd(
|
|
|
582
574
|
emit(output, is_error=_is_read_error_output)
|
|
583
575
|
|
|
584
576
|
|
|
585
|
-
@main.group(name="dynamic-frontend")
|
|
586
|
-
def dynamic_frontend() -> None:
|
|
587
|
-
"""Deploy, list, and delete Dynamic Frontend spaces."""
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
@dynamic_frontend.command(name="deploy")
|
|
591
|
-
@click.option(
|
|
592
|
-
"--file",
|
|
593
|
-
"file_path",
|
|
594
|
-
default=None,
|
|
595
|
-
type=click.Path(exists=True),
|
|
596
|
-
help="Path to an upload-ready Dynamic Frontend ZIP bundle.",
|
|
597
|
-
)
|
|
598
|
-
@click.option(
|
|
599
|
-
"--content-id",
|
|
600
|
-
default=None,
|
|
601
|
-
help="Existing Knowledge Base content id for the ZIP bundle.",
|
|
602
|
-
)
|
|
603
|
-
@click.option(
|
|
604
|
-
"--name",
|
|
605
|
-
default=None,
|
|
606
|
-
help="Space display name. Required when creating; optional rename when updating.",
|
|
607
|
-
)
|
|
608
|
-
@click.option(
|
|
609
|
-
"--space-id", default=None, help="Existing Dynamic Frontend space id to update."
|
|
610
|
-
)
|
|
611
|
-
@click.option(
|
|
612
|
-
"--json", "output_json", is_flag=True, default=False, help="Print raw JSON."
|
|
613
|
-
)
|
|
614
|
-
@click.pass_context
|
|
615
|
-
def dynamic_frontend_deploy(
|
|
616
|
-
ctx: click.Context,
|
|
617
|
-
file_path: str | None,
|
|
618
|
-
content_id: str | None,
|
|
619
|
-
name: str | None,
|
|
620
|
-
space_id: str | None,
|
|
621
|
-
output_json: bool,
|
|
622
|
-
) -> None:
|
|
623
|
-
"""Create or update a Dynamic Frontend space.
|
|
624
|
-
|
|
625
|
-
\b
|
|
626
|
-
Examples:
|
|
627
|
-
unique-cli dynamic-frontend deploy --file ./app.zip --name "Revenue Dashboard"
|
|
628
|
-
unique-cli dynamic-frontend deploy --content-id content_123 --name "Revenue Dashboard"
|
|
629
|
-
unique-cli dynamic-frontend deploy --space-id assistant_123 --file ./app.zip
|
|
630
|
-
"""
|
|
631
|
-
output = cmd_dynamic_frontend_deploy(
|
|
632
|
-
LazyState.get(ctx),
|
|
633
|
-
file=file_path,
|
|
634
|
-
content_id=content_id,
|
|
635
|
-
name=name,
|
|
636
|
-
space_id=space_id,
|
|
637
|
-
output_json=output_json,
|
|
638
|
-
)
|
|
639
|
-
click.echo(output)
|
|
640
|
-
if output.startswith(_DYNAMIC_FRONTEND_ERROR_PREFIX):
|
|
641
|
-
ctx.exit(1)
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
@dynamic_frontend.command(name="list")
|
|
645
|
-
@click.option(
|
|
646
|
-
"--json", "output_json", is_flag=True, default=False, help="Print raw JSON."
|
|
647
|
-
)
|
|
648
|
-
@click.pass_context
|
|
649
|
-
def dynamic_frontend_list(ctx: click.Context, output_json: bool) -> None:
|
|
650
|
-
"""List Dynamic Frontend spaces the current user can manage."""
|
|
651
|
-
output = cmd_dynamic_frontend_list(LazyState.get(ctx), output_json=output_json)
|
|
652
|
-
click.echo(output)
|
|
653
|
-
if output.startswith(_DYNAMIC_FRONTEND_ERROR_PREFIX):
|
|
654
|
-
ctx.exit(1)
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
@dynamic_frontend.command(name="delete")
|
|
658
|
-
@click.argument("space_id")
|
|
659
|
-
@click.option(
|
|
660
|
-
"--json", "output_json", is_flag=True, default=False, help="Print raw JSON."
|
|
661
|
-
)
|
|
662
|
-
@click.pass_context
|
|
663
|
-
def dynamic_frontend_delete(
|
|
664
|
-
ctx: click.Context, space_id: str, output_json: bool
|
|
665
|
-
) -> None:
|
|
666
|
-
"""Delete a deployed Dynamic Frontend space by its space id.
|
|
667
|
-
|
|
668
|
-
\b
|
|
669
|
-
Example:
|
|
670
|
-
unique-cli dynamic-frontend delete assistant_123
|
|
671
|
-
"""
|
|
672
|
-
output = cmd_dynamic_frontend_delete(
|
|
673
|
-
LazyState.get(ctx), space_id, output_json=output_json
|
|
674
|
-
)
|
|
675
|
-
click.echo(output)
|
|
676
|
-
if output.startswith(_DYNAMIC_FRONTEND_ERROR_PREFIX):
|
|
677
|
-
ctx.exit(1)
|
|
678
|
-
|
|
679
|
-
|
|
680
577
|
@main.command()
|
|
681
578
|
@click.argument("name_or_id")
|
|
682
579
|
@click.pass_context
|
|
@@ -1257,9 +1154,24 @@ def elicit() -> None:
|
|
|
1257
1154
|
default=DEFAULT_WAIT_TIMEOUT_SECONDS,
|
|
1258
1155
|
show_default=True,
|
|
1259
1156
|
help=(
|
|
1260
|
-
"Max seconds to block waiting for the user's response.
|
|
1261
|
-
"sets when the elicitation expires, so the
|
|
1262
|
-
"when we stop waiting and the chat UI can
|
|
1157
|
+
"Max seconds to block waiting for the user's response. Without "
|
|
1158
|
+
"--expires-in, this also sets when the elicitation expires, so the "
|
|
1159
|
+
"request expires exactly when we stop waiting and the chat UI can "
|
|
1160
|
+
"offer a way to continue."
|
|
1161
|
+
),
|
|
1162
|
+
)
|
|
1163
|
+
@click.option(
|
|
1164
|
+
"--expires-in",
|
|
1165
|
+
"expires_in_seconds",
|
|
1166
|
+
type=int,
|
|
1167
|
+
default=None,
|
|
1168
|
+
help=(
|
|
1169
|
+
"Seconds before the platform expires the request, decoupled from "
|
|
1170
|
+
"--timeout (the local wait budget). Use this when this process's "
|
|
1171
|
+
"wait budget is shorter than how long a human should realistically "
|
|
1172
|
+
"get to answer (e.g. a harness with its own foreground timeout) -- "
|
|
1173
|
+
"otherwise the request expires under the user before they can "
|
|
1174
|
+
"answer. Defaults to --timeout, exactly matching prior behavior."
|
|
1263
1175
|
),
|
|
1264
1176
|
)
|
|
1265
1177
|
@click.option(
|
|
@@ -1320,6 +1232,7 @@ def elicit_ask(
|
|
|
1320
1232
|
chat_id: str | None,
|
|
1321
1233
|
message_id: str | None,
|
|
1322
1234
|
timeout: int,
|
|
1235
|
+
expires_in_seconds: int | None,
|
|
1323
1236
|
poll_interval: float,
|
|
1324
1237
|
metadata: tuple[str, ...],
|
|
1325
1238
|
visible: bool = True,
|
|
@@ -1332,7 +1245,9 @@ def elicit_ask(
|
|
|
1332
1245
|
\b
|
|
1333
1246
|
Creates a FORM elicitation in the Unique UI with the given MESSAGE
|
|
1334
1247
|
and blocks until the user responds, the elicitation is declined /
|
|
1335
|
-
cancelled / expired, or --timeout is reached.
|
|
1248
|
+
cancelled / expired, or --timeout is reached. Immediately after
|
|
1249
|
+
creation, before polling starts, a single line is written to stderr:
|
|
1250
|
+
"UNIQUE_ELICITATION_CREATED id=<id> expires_at=<iso8601>".
|
|
1336
1251
|
|
|
1337
1252
|
\b
|
|
1338
1253
|
Examples:
|
|
@@ -1340,6 +1255,8 @@ def elicit_ask(
|
|
|
1340
1255
|
unique-cli elicit ask "Confirm deletion of /Archive" --timeout 60
|
|
1341
1256
|
unique-cli elicit ask "Pick a region" \\
|
|
1342
1257
|
--schema '{"type":"object","properties":{"region":{"type":"string","enum":["EU","US","APAC"]}},"required":["region"]}'
|
|
1258
|
+
unique-cli elicit ask "Long-running approval" \\
|
|
1259
|
+
--expires-in 7200 --timeout 300
|
|
1343
1260
|
"""
|
|
1344
1261
|
parsed_metadata: list[tuple[str, str]] = []
|
|
1345
1262
|
for kv in metadata:
|
|
@@ -1356,6 +1273,7 @@ def elicit_ask(
|
|
|
1356
1273
|
"chat_id": chat_id,
|
|
1357
1274
|
"message_id": _resolve_cli_message_id(ctx, message_id),
|
|
1358
1275
|
"timeout": timeout,
|
|
1276
|
+
"expires_in_seconds": expires_in_seconds,
|
|
1359
1277
|
"poll_interval": poll_interval,
|
|
1360
1278
|
"metadata": parsed_metadata or None,
|
|
1361
1279
|
"visible": visible,
|
{unique_sdk-2026.32.0.dev8 → unique_sdk-2026.32.0.dev10}/unique_sdk/cli/commands/elicitation.py
RENAMED
|
@@ -13,8 +13,9 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
import json
|
|
15
15
|
import os
|
|
16
|
+
import sys
|
|
16
17
|
import time
|
|
17
|
-
from datetime import datetime, timezone
|
|
18
|
+
from datetime import datetime, timedelta, timezone
|
|
18
19
|
from typing import Any, Literal, cast
|
|
19
20
|
|
|
20
21
|
import unique_sdk
|
|
@@ -37,6 +38,23 @@ TERMINAL_STATUSES = {
|
|
|
37
38
|
"COMPLETED",
|
|
38
39
|
}
|
|
39
40
|
|
|
41
|
+
# --- Transient-failure retry (UN-23310) ---------------------------------
|
|
42
|
+
#
|
|
43
|
+
# ``cmd_elicit_wait`` used to return on the very first ``APIError`` /
|
|
44
|
+
# ``APIConnectionError`` raised mid-poll, treating a transient 502 the same
|
|
45
|
+
# as a genuine terminal condition. Now that a single ``elicit ask`` call is
|
|
46
|
+
# expected to carry the whole wait unsupervised, that one dropped connection
|
|
47
|
+
# would otherwise cost the entire turn. These constants bound the retry
|
|
48
|
+
# loop's backoff; the overall ``--timeout`` remains the hard outer bound.
|
|
49
|
+
TRANSIENT_RETRY_BASE_SECONDS = 1.0
|
|
50
|
+
TRANSIENT_RETRY_MAX_BACKOFF_SECONDS = 30.0
|
|
51
|
+
|
|
52
|
+
# Prefix for the machine-readable line ``cmd_elicit_ask`` writes to stderr
|
|
53
|
+
# immediately after creating the elicitation, before it starts polling. Kept
|
|
54
|
+
# as a module constant so producer (here) and any future consumer/tests stay
|
|
55
|
+
# in lockstep on the exact contract string.
|
|
56
|
+
ELICITATION_CREATED_STDERR_PREFIX = "UNIQUE_ELICITATION_CREATED"
|
|
57
|
+
|
|
40
58
|
# --- Visibility workaround for UN-19815 ---------------------------------
|
|
41
59
|
#
|
|
42
60
|
# The chat UI (as of 2026-04-21) only renders an elicitation when the host
|
|
@@ -632,6 +650,74 @@ def cmd_elicit_respond(
|
|
|
632
650
|
return f"elicit: {exc}"
|
|
633
651
|
|
|
634
652
|
|
|
653
|
+
def _is_transient_api_failure(exc: BaseException) -> bool:
|
|
654
|
+
"""Return ``True`` if *exc* is worth retrying.
|
|
655
|
+
|
|
656
|
+
Transient: connection-level failures (timeouts, resets --
|
|
657
|
+
``APIConnectionError``) and ``APIError``s with no HTTP status or a 5xx
|
|
658
|
+
status. Not transient: 4xx client errors, which cannot succeed on
|
|
659
|
+
retry -- retrying those would just burn the wait budget on a request
|
|
660
|
+
that is doomed regardless.
|
|
661
|
+
|
|
662
|
+
Note ``InvalidRequestError`` / ``AuthenticationError`` / ``PermissionError``
|
|
663
|
+
are not ``APIError`` subclasses and are not matched here; they propagate
|
|
664
|
+
unchanged, exactly as they did before this retry logic existed.
|
|
665
|
+
"""
|
|
666
|
+
if isinstance(exc, unique_sdk.APIConnectionError):
|
|
667
|
+
return True
|
|
668
|
+
if isinstance(exc, unique_sdk.APIError):
|
|
669
|
+
status = getattr(exc, "http_status", None)
|
|
670
|
+
return status is None or status >= 500
|
|
671
|
+
return False
|
|
672
|
+
|
|
673
|
+
|
|
674
|
+
def _get_elicitation_with_retry(
|
|
675
|
+
state: ShellState,
|
|
676
|
+
elicitation_id: str,
|
|
677
|
+
*,
|
|
678
|
+
deadline: float,
|
|
679
|
+
) -> dict[str, Any]:
|
|
680
|
+
"""Fetch an elicitation, retrying transient failures with backoff.
|
|
681
|
+
|
|
682
|
+
Retries are bounded by *deadline* (a ``time.monotonic()`` timestamp) --
|
|
683
|
+
once it passes, the last exception is re-raised so the caller's overall
|
|
684
|
+
``--timeout`` is always the hard outer bound, never exceeded by retries.
|
|
685
|
+
Non-transient failures (see :func:`_is_transient_api_failure`) are
|
|
686
|
+
re-raised immediately without retrying. Each retry is logged to stderr
|
|
687
|
+
with the attempt number and backoff so a string of dropped connections
|
|
688
|
+
is diagnosable instead of silently eating the wait budget.
|
|
689
|
+
"""
|
|
690
|
+
backoff = TRANSIENT_RETRY_BASE_SECONDS
|
|
691
|
+
attempt = 0
|
|
692
|
+
while True:
|
|
693
|
+
try:
|
|
694
|
+
return dict(
|
|
695
|
+
unique_sdk.Elicitation.get_elicitation(
|
|
696
|
+
user_id=state.config.user_id,
|
|
697
|
+
company_id=state.config.company_id,
|
|
698
|
+
elicitation_id=elicitation_id,
|
|
699
|
+
)
|
|
700
|
+
)
|
|
701
|
+
except (unique_sdk.APIError, unique_sdk.APIConnectionError) as exc:
|
|
702
|
+
if not _is_transient_api_failure(exc):
|
|
703
|
+
raise
|
|
704
|
+
remaining = deadline - time.monotonic()
|
|
705
|
+
if remaining <= 0:
|
|
706
|
+
raise
|
|
707
|
+
attempt += 1
|
|
708
|
+
sleep_for = max(
|
|
709
|
+
0.0, min(backoff, TRANSIENT_RETRY_MAX_BACKOFF_SECONDS, remaining)
|
|
710
|
+
)
|
|
711
|
+
print(
|
|
712
|
+
f"elicit: transient error on attempt {attempt} fetching "
|
|
713
|
+
f"{elicitation_id} ({exc}); retrying in {sleep_for:.1f}s",
|
|
714
|
+
file=sys.stderr,
|
|
715
|
+
)
|
|
716
|
+
if sleep_for > 0:
|
|
717
|
+
time.sleep(sleep_for)
|
|
718
|
+
backoff = min(backoff * 2, TRANSIENT_RETRY_MAX_BACKOFF_SECONDS)
|
|
719
|
+
|
|
720
|
+
|
|
635
721
|
def cmd_elicit_wait(
|
|
636
722
|
state: ShellState,
|
|
637
723
|
elicitation_id: str,
|
|
@@ -658,12 +744,10 @@ def cmd_elicit_wait(
|
|
|
658
744
|
try:
|
|
659
745
|
deadline = time.monotonic() + max(1, timeout)
|
|
660
746
|
while True:
|
|
661
|
-
elicitation =
|
|
662
|
-
|
|
663
|
-
company_id=state.config.company_id,
|
|
664
|
-
elicitation_id=elicitation_id,
|
|
747
|
+
elicitation = _get_elicitation_with_retry(
|
|
748
|
+
state, elicitation_id, deadline=deadline
|
|
665
749
|
)
|
|
666
|
-
last =
|
|
750
|
+
last = elicitation
|
|
667
751
|
status = str(elicitation.get("status", "")).upper()
|
|
668
752
|
if status in TERMINAL_STATUSES:
|
|
669
753
|
terminal_status = status
|
|
@@ -675,12 +759,8 @@ def cmd_elicit_wait(
|
|
|
675
759
|
# same instant; this fetch forces the backend's lazy expiry to
|
|
676
760
|
# run so we report a clean EXPIRED — and publish it to the chat
|
|
677
761
|
# subscription — instead of a stale PENDING.
|
|
678
|
-
final =
|
|
679
|
-
|
|
680
|
-
user_id=state.config.user_id,
|
|
681
|
-
company_id=state.config.company_id,
|
|
682
|
-
elicitation_id=elicitation_id,
|
|
683
|
-
)
|
|
762
|
+
final = _get_elicitation_with_retry(
|
|
763
|
+
state, elicitation_id, deadline=deadline
|
|
684
764
|
)
|
|
685
765
|
last = final
|
|
686
766
|
final_status = str(final.get("status", "")).upper()
|
|
@@ -703,7 +783,7 @@ def cmd_elicit_wait(
|
|
|
703
783
|
f"{format_elicitation(last)}"
|
|
704
784
|
)
|
|
705
785
|
time.sleep(poll_interval)
|
|
706
|
-
except unique_sdk.APIError as exc:
|
|
786
|
+
except (unique_sdk.APIError, unique_sdk.APIConnectionError) as exc:
|
|
707
787
|
return f"elicit: {exc}"
|
|
708
788
|
finally:
|
|
709
789
|
# If the poll loop exited before we ever received a response
|
|
@@ -720,7 +800,7 @@ def cmd_elicit_wait(
|
|
|
720
800
|
elicitation_id=elicitation_id,
|
|
721
801
|
)
|
|
722
802
|
)
|
|
723
|
-
except unique_sdk.APIError:
|
|
803
|
+
except (unique_sdk.APIError, unique_sdk.APIConnectionError):
|
|
724
804
|
last = None
|
|
725
805
|
ctx = _extract_visibility_context(last)
|
|
726
806
|
if ctx is not None:
|
|
@@ -735,6 +815,36 @@ def cmd_elicit_wait(
|
|
|
735
815
|
)
|
|
736
816
|
|
|
737
817
|
|
|
818
|
+
def _emit_elicitation_created_stderr_line(
|
|
819
|
+
elicitation_id: str,
|
|
820
|
+
expires_at: object,
|
|
821
|
+
expires_in_seconds: int,
|
|
822
|
+
) -> None:
|
|
823
|
+
"""Write the ``UNIQUE_ELICITATION_CREATED`` contract line to stderr.
|
|
824
|
+
|
|
825
|
+
Format (stable, greppable, one line, stderr only)::
|
|
826
|
+
|
|
827
|
+
UNIQUE_ELICITATION_CREATED id=<id> expires_at=<iso8601>
|
|
828
|
+
|
|
829
|
+
Prefers the platform-returned ``expiresAt`` (authoritative); falls back
|
|
830
|
+
to a locally computed timestamp if the platform omitted it, so the line
|
|
831
|
+
is always emitted with a usable value.
|
|
832
|
+
"""
|
|
833
|
+
if isinstance(expires_at, str) and expires_at:
|
|
834
|
+
resolved_expires_at = expires_at
|
|
835
|
+
else:
|
|
836
|
+
resolved_expires_at = (
|
|
837
|
+
(datetime.now(tz=timezone.utc) + timedelta(seconds=expires_in_seconds))
|
|
838
|
+
.isoformat(timespec="milliseconds")
|
|
839
|
+
.replace("+00:00", "Z")
|
|
840
|
+
)
|
|
841
|
+
print(
|
|
842
|
+
f"{ELICITATION_CREATED_STDERR_PREFIX} id={elicitation_id} "
|
|
843
|
+
f"expires_at={resolved_expires_at}",
|
|
844
|
+
file=sys.stderr,
|
|
845
|
+
)
|
|
846
|
+
|
|
847
|
+
|
|
738
848
|
def cmd_elicit_ask(
|
|
739
849
|
state: ShellState,
|
|
740
850
|
*,
|
|
@@ -745,6 +855,7 @@ def cmd_elicit_ask(
|
|
|
745
855
|
message_id: str | None = None,
|
|
746
856
|
timeout: int = DEFAULT_WAIT_TIMEOUT_SECONDS,
|
|
747
857
|
poll_interval: float = DEFAULT_POLL_INTERVAL_SECONDS,
|
|
858
|
+
expires_in_seconds: int | None = None,
|
|
748
859
|
metadata: list[tuple[str, str]] | None = None,
|
|
749
860
|
visible: bool = True,
|
|
750
861
|
assistant_id: str | None = None,
|
|
@@ -757,6 +868,23 @@ def cmd_elicit_ask(
|
|
|
757
868
|
free-text ``answer`` is used. This is the preferred entry point when an
|
|
758
869
|
agent needs to ask the user a clarifying question.
|
|
759
870
|
|
|
871
|
+
``expires_in_seconds``, when given, decouples the server-side expiry
|
|
872
|
+
(``expiresInSeconds`` sent to the platform) from ``timeout`` (how long
|
|
873
|
+
*this process* blocks polling). When omitted (the default), behaviour is
|
|
874
|
+
unchanged from before this parameter existed: the elicitation expires
|
|
875
|
+
exactly when the local wait gives up, coupling the two. Pass this when
|
|
876
|
+
the caller's own wait budget (e.g. a harness's foreground timeout) is
|
|
877
|
+
shorter than how long a human should realistically have to answer --
|
|
878
|
+
otherwise the elicitation expires under the user before they see it.
|
|
879
|
+
|
|
880
|
+
Immediately after creation (before polling starts), a single line is
|
|
881
|
+
written to stderr for callers that want the elicitation id without
|
|
882
|
+
polling the pending list:
|
|
883
|
+
|
|
884
|
+
UNIQUE_ELICITATION_CREATED id=<id> expires_at=<iso8601>
|
|
885
|
+
|
|
886
|
+
This is stderr-only and does not change stdout's format.
|
|
887
|
+
|
|
760
888
|
When ``chat_id`` is set and ``visible`` is ``True`` (the default), the
|
|
761
889
|
elicitation is wrapped in a placeholder thinking timeline so the chat UI
|
|
762
890
|
actually renders it — see the "Visibility workaround for UN-19815" note
|
|
@@ -778,16 +906,17 @@ def cmd_elicit_ask(
|
|
|
778
906
|
"required": ["answer"],
|
|
779
907
|
}
|
|
780
908
|
|
|
781
|
-
# `ask` exposes a single knob: `--timeout` is both how long
|
|
782
|
-
# when the record expires. The two are
|
|
783
|
-
#
|
|
784
|
-
#
|
|
785
|
-
#
|
|
786
|
-
#
|
|
787
|
-
#
|
|
788
|
-
#
|
|
789
|
-
|
|
790
|
-
|
|
909
|
+
# By default `ask` exposes a single knob: `--timeout` is both how long
|
|
910
|
+
# we wait and when the record expires. The two are the same unless the
|
|
911
|
+
# caller passes `--expires-in` explicitly, so the backend's default (5
|
|
912
|
+
# minutes) never leaves the record PENDING after we have stopped
|
|
913
|
+
# waiting — which would otherwise prevent the chat UI from flipping
|
|
914
|
+
# the prompt to EXPIRED and offering the user a way to continue. The
|
|
915
|
+
# poll loop below reads the freshly EXPIRED record and the elicitation
|
|
916
|
+
# subscription delivers the terminal status to the chat.
|
|
917
|
+
effective_expires_in_seconds = (
|
|
918
|
+
expires_in_seconds if expires_in_seconds is not None else timeout
|
|
919
|
+
)
|
|
791
920
|
|
|
792
921
|
user_metadata = _parse_metadata_pairs(metadata)
|
|
793
922
|
effective_message_id = message_id
|
|
@@ -836,7 +965,7 @@ def cmd_elicit_ask(
|
|
|
836
965
|
url=None,
|
|
837
966
|
chat_id=chat_id,
|
|
838
967
|
message_id=effective_message_id,
|
|
839
|
-
expires_in_seconds=
|
|
968
|
+
expires_in_seconds=effective_expires_in_seconds,
|
|
840
969
|
external_elicitation_id=None,
|
|
841
970
|
metadata=user_metadata,
|
|
842
971
|
)
|
|
@@ -856,6 +985,12 @@ def cmd_elicit_ask(
|
|
|
856
985
|
_cleanup_placeholder_if_needed()
|
|
857
986
|
return "elicit: platform did not return an elicitation id"
|
|
858
987
|
|
|
988
|
+
_emit_elicitation_created_stderr_line(
|
|
989
|
+
elicitation_id,
|
|
990
|
+
elicitation.get("expiresAt"),
|
|
991
|
+
effective_expires_in_seconds,
|
|
992
|
+
)
|
|
993
|
+
|
|
859
994
|
# ``cmd_elicit_wait`` handles its own placeholder teardown by
|
|
860
995
|
# reading the markers back from the elicitation's metadata.
|
|
861
996
|
return cmd_elicit_wait(
|
|
@@ -89,7 +89,10 @@ OVERVIEW_HELP = textwrap.dedent("""\
|
|
|
89
89
|
--chat-id / -c <id> Associated chat ID
|
|
90
90
|
--message-id / -m <id> Associated message ID
|
|
91
91
|
--timeout <seconds> Max wait time, also sets when the
|
|
92
|
-
request expires
|
|
92
|
+
request expires unless --expires-in
|
|
93
|
+
is given (default: 7200)
|
|
94
|
+
--expires-in <seconds> Decouple server-side expiry from
|
|
95
|
+
--timeout (defaults to --timeout)
|
|
93
96
|
--poll-interval <seconds> Poll frequency (default: 2.0)
|
|
94
97
|
--metadata key=value Metadata (repeatable)
|
|
95
98
|
--no-visible Skip the UN-19815 visibility workaround
|
|
@@ -814,9 +817,12 @@ class UniqueShell(cmd.Cmd):
|
|
|
814
817
|
--mode FORM|URL Display mode (create only, default: FORM)
|
|
815
818
|
--chat-id / -c <id> Associated chat ID
|
|
816
819
|
--message-id / -m <id> Associated message ID
|
|
817
|
-
--expires-in <seconds> Auto-expire the request
|
|
820
|
+
--expires-in <seconds> Auto-expire the request. For ask, this
|
|
821
|
+
decouples expiry from --timeout
|
|
822
|
+
(defaults to --timeout when omitted)
|
|
818
823
|
--timeout <seconds> (ask / wait) max wait time, default 7200;
|
|
819
824
|
for ask this also sets when it expires
|
|
825
|
+
unless --expires-in is given
|
|
820
826
|
--poll-interval <seconds> (ask / wait) poll frequency, default 2
|
|
821
827
|
--external-id <id> External identifier (create only)
|
|
822
828
|
--metadata key=value Metadata (repeatable)
|
|
@@ -999,13 +1005,6 @@ class UniqueShell(cmd.Cmd):
|
|
|
999
1005
|
if not message:
|
|
1000
1006
|
self._print("Usage: elicit ask <message> [options]")
|
|
1001
1007
|
return
|
|
1002
|
-
if opts["expires_in_seconds"] is not None:
|
|
1003
|
-
self._print(
|
|
1004
|
-
"No such option: --expires-in for 'elicit ask'. Use --timeout "
|
|
1005
|
-
"(it also sets when the request expires). For expiry decoupled "
|
|
1006
|
-
"from a local wait, use 'elicit create --expires-in'."
|
|
1007
|
-
)
|
|
1008
|
-
return
|
|
1009
1008
|
|
|
1010
1009
|
ask_kwargs: dict[str, Any] = {
|
|
1011
1010
|
"message": message,
|
|
@@ -1014,6 +1013,7 @@ class UniqueShell(cmd.Cmd):
|
|
|
1014
1013
|
"chat_id": opts["chat_id"],
|
|
1015
1014
|
"message_id": opts["message_id"],
|
|
1016
1015
|
"timeout": opts["timeout"],
|
|
1016
|
+
"expires_in_seconds": opts["expires_in_seconds"],
|
|
1017
1017
|
"poll_interval": opts["poll_interval"],
|
|
1018
1018
|
"metadata": opts["metadata"] or None,
|
|
1019
1019
|
"visible": opts["visible"],
|
|
@@ -235,24 +235,29 @@ If you obtained the content some other way (you `download`ed the raw bytes and p
|
|
|
235
235
|
CSV, `.txt`, HTML, images) have no pages — always omit `--pages`** and cite the
|
|
236
236
|
whole file.
|
|
237
237
|
|
|
238
|
-
**Choosing `--read-method`** (declare the *representation*
|
|
238
|
+
**Choosing `--read-method`** (declare the *representation* you actually read the
|
|
239
|
+
cited value from). Pick it by the **modality you read** — NOT by how you *located*
|
|
240
|
+
the page. Locating a page with `unique-cli read` does **not** force `indexed`: if you
|
|
241
|
+
then rendered that page and read a chart/figure with vision, the method is `vision`.
|
|
239
242
|
|
|
240
243
|
- `text` → you used **extracted text** (`pdftotext`, PyMuPDF / `fitz` `page.get_text()`, MarkItDown, or any text extraction).
|
|
241
|
-
- `vision` → you read a **rendered image** of the page/slide (e.g. `get_pixmap()`) with your vision capability.
|
|
242
|
-
- `indexed` → you
|
|
244
|
+
- `vision` → you read a **rendered image** of the page/slide (e.g. `get_pixmap()`) with your vision capability — including when you located the page via `unique-cli read` but then rendered it to read a chart, figure, table, or scanned page.
|
|
245
|
+
- `indexed` → you used the **text returned by `unique-cli read`** (the platform's indexed chunks) as your answer source.
|
|
243
246
|
|
|
244
247
|
| Value | When to use |
|
|
245
248
|
|-------|-------------|
|
|
246
|
-
| `text` | You
|
|
247
|
-
| `vision` | You
|
|
248
|
-
| `indexed` | You
|
|
249
|
-
|
|
250
|
-
**Verify page numbers before citing — unless you
|
|
251
|
-
|
|
252
|
-
`[p.N]` / `[p.N-M]` markers are already
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
249
|
+
| `text` | You extracted the page/document text yourself and used that text. |
|
|
250
|
+
| `vision` | You read a rendered image of the page with your vision capability — even if you located the page via `unique-cli read`. |
|
|
251
|
+
| `indexed` | You used the text returned by `unique-cli read` (indexed chunks) as your source. |
|
|
252
|
+
|
|
253
|
+
**Verify page numbers before citing — unless you located them via `unique-cli read`.**
|
|
254
|
+
This is about the *page number* only, and is **independent of `--read-method`**. If you
|
|
255
|
+
located the page with `unique-cli read`, its `[p.N]` / `[p.N-M]` markers are already
|
|
256
|
+
physical positions — trust them and skip the checks below, even if you then rendered the
|
|
257
|
+
page and read it with vision (in that case still cite `--read-method vision`). Only when
|
|
258
|
+
you obtained the page some other way — you `download`ed the raw bytes and parsed them
|
|
259
|
+
yourself, or `read` returned no `[p.N]` markers — pick the row matching the file you read
|
|
260
|
+
and verify the cited content really is where you claim before calling `cite`:
|
|
256
261
|
|
|
257
262
|
- **PDF** — `pdfinfo file.pdf | grep Pages` for the total physical page count, then
|
|
258
263
|
for **each** page run `pdftotext -f N -l N file.pdf -` and confirm the content is
|
|
@@ -265,11 +270,13 @@ verify the cited content really is where you claim before calling `cite`:
|
|
|
265
270
|
- **Non-paginated (XLSX/CSV/TXT/HTML/images)** — there are no pages. Do **NOT**
|
|
266
271
|
pass `--pages`; cite the whole file and verify the content exists in it.
|
|
267
272
|
|
|
268
|
-
Then determine `--read-method
|
|
269
|
-
fallback chain (e.g. text extraction returned nothing →
|
|
270
|
-
report `text` if you used extracted text or `vision` if you read
|
|
271
|
-
|
|
272
|
-
|
|
273
|
+
Then determine `--read-method` by the **modality you actually read** — independent of
|
|
274
|
+
how you located the page. In a fallback chain (e.g. text extraction returned nothing →
|
|
275
|
+
render + read visually), report `text` if you used extracted text or `vision` if you read
|
|
276
|
+
a rendered image. If you located the page via `unique-cli read` but read the cited value
|
|
277
|
+
off a rendered image (a chart, figure, table, or scanned page), report `vision`, not
|
|
278
|
+
`indexed`. Only after confirming the page numbers, call `unique-cli cite` with the
|
|
279
|
+
verified page numbers (if any) and `--read-method`.
|
|
273
280
|
|
|
274
281
|
- **One method per `cite` call.** If different pages were read with different methods, issue separate `cite` calls — one per method.
|
|
275
282
|
- Numbers are **per-turn only**; do not reuse from prior turns.
|