cassis-cli 3.4.0__tar.gz → 3.5.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.
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/PKG-INFO +45 -1
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/README.md +44 -0
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/cassis_cli/api.py +53 -0
- cassis_cli-3.5.0/cassis_cli/chats.py +204 -0
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/cassis_cli/main.py +2 -0
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/pyproject.toml +1 -1
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/LICENSE +0 -0
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/NOTICE +0 -0
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/cassis_cli/__init__.py +0 -0
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/cassis_cli/common.py +0 -0
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/cassis_cli/dbt.py +0 -0
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/cassis_cli/eval.py +0 -0
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/cassis_cli/guide.py +0 -0
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/cassis_cli/issues.py +0 -0
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/cassis_cli/ontology.py +0 -0
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/cassis_cli/ontology_design_guide.md +0 -0
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/cassis_cli/projects.py +0 -0
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/cassis_cli/schema.py +0 -0
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/cassis_cli/schema_plan.py +0 -0
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/cassis_cli/status.py +0 -0
- {cassis_cli-3.4.0 → cassis_cli-3.5.0}/cassis_cli/verify.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cassis-cli
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.5.0
|
|
4
4
|
Summary: Validate, test and evaluate your Cassis context from your terminal, then publish it
|
|
5
5
|
License: Apache-2.0
|
|
6
6
|
License-File: LICENSE
|
|
@@ -348,3 +348,47 @@ requires an account.
|
|
|
348
348
|
|
|
349
349
|
Apache License 2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
|
350
350
|
|
|
351
|
+
|
|
352
|
+
## Read conversations and their analyses
|
|
353
|
+
|
|
354
|
+
These commands read saved conversations and reports. They never start an analysis,
|
|
355
|
+
retry a failed run, execute SQL or make a model call. Use the same API key and
|
|
356
|
+
project selection as other commands (`CASSIS_API_KEY`, `--project`, or the
|
|
357
|
+
checkout's `cassis/project.yml`). Requires a server with the conversation API.
|
|
358
|
+
|
|
359
|
+
```bash
|
|
360
|
+
cassis chats list --limit 20
|
|
361
|
+
cassis chats list --review needs-attention --feedback down --since 2026-11-01
|
|
362
|
+
cassis chats list --search revenue --offset 20
|
|
363
|
+
cassis chats show <chat-id>
|
|
364
|
+
cassis chats show <chat-id> --analysis-only
|
|
365
|
+
cassis chats show <chat-id> --trace
|
|
366
|
+
cassis chats show <chat-id> --trace-entry trace:<message-id>:<index>
|
|
367
|
+
cassis chats show <chat-id> --json
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
`list` sorts by latest message, returns up to 20 chats by default (maximum 100
|
|
371
|
+
per page), and reports the total and next offset. Filters combine with AND.
|
|
372
|
+
`--feedback` accepts `up`, `down`, `any`, or `none`; a conversation with both
|
|
373
|
+
positive and negative feedback matches both `up` and `down`. `--review
|
|
374
|
+
needs-attention` selects completed latest reports containing a potential problem,
|
|
375
|
+
not improvement suggestions alone. `--since` includes conversations created or
|
|
376
|
+
with a message posted at or after the given ISO date or timestamp; dates and
|
|
377
|
+
naive timestamps use UTC.
|
|
378
|
+
|
|
379
|
+
`show` prints messages, generated SQL, feedback, then the saved analysis with its
|
|
380
|
+
citations and next steps. Traces are compact by default; `--trace` adds previews
|
|
381
|
+
limited to 500 characters per entry, and `--trace-entry` opens one entry in full.
|
|
382
|
+
The entry identifiers match the analysis's trace references. `--json` returns the
|
|
383
|
+
full messages and traces without those display limits; `--analysis-only --json`
|
|
384
|
+
returns the analysis view. If a retry failed, the previous completed report is
|
|
385
|
+
shown with the failed status. Missing analyses are reported explicitly.
|
|
386
|
+
|
|
387
|
+
Members can read their own chats; administrators can read chats in accessible
|
|
388
|
+
projects. API keys scoped to a project cannot reach another one. `--app-url`
|
|
389
|
+
sets the base URL for browser links when the webapp and API use different hosts
|
|
390
|
+
or ports. JSON list responses include `url` on each item and `next_offset`.
|
|
391
|
+
Exit codes: 0 on a successful read (even with no report or an empty list), 1 for
|
|
392
|
+
an inaccessible/missing chat or trace entry, 2 for invalid arguments, 3 for API
|
|
393
|
+
or authentication errors.
|
|
394
|
+
|
|
@@ -325,3 +325,47 @@ requires an account.
|
|
|
325
325
|
## License
|
|
326
326
|
|
|
327
327
|
Apache License 2.0. See [LICENSE](LICENSE) and [NOTICE](NOTICE).
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
## Read conversations and their analyses
|
|
331
|
+
|
|
332
|
+
These commands read saved conversations and reports. They never start an analysis,
|
|
333
|
+
retry a failed run, execute SQL or make a model call. Use the same API key and
|
|
334
|
+
project selection as other commands (`CASSIS_API_KEY`, `--project`, or the
|
|
335
|
+
checkout's `cassis/project.yml`). Requires a server with the conversation API.
|
|
336
|
+
|
|
337
|
+
```bash
|
|
338
|
+
cassis chats list --limit 20
|
|
339
|
+
cassis chats list --review needs-attention --feedback down --since 2026-11-01
|
|
340
|
+
cassis chats list --search revenue --offset 20
|
|
341
|
+
cassis chats show <chat-id>
|
|
342
|
+
cassis chats show <chat-id> --analysis-only
|
|
343
|
+
cassis chats show <chat-id> --trace
|
|
344
|
+
cassis chats show <chat-id> --trace-entry trace:<message-id>:<index>
|
|
345
|
+
cassis chats show <chat-id> --json
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
`list` sorts by latest message, returns up to 20 chats by default (maximum 100
|
|
349
|
+
per page), and reports the total and next offset. Filters combine with AND.
|
|
350
|
+
`--feedback` accepts `up`, `down`, `any`, or `none`; a conversation with both
|
|
351
|
+
positive and negative feedback matches both `up` and `down`. `--review
|
|
352
|
+
needs-attention` selects completed latest reports containing a potential problem,
|
|
353
|
+
not improvement suggestions alone. `--since` includes conversations created or
|
|
354
|
+
with a message posted at or after the given ISO date or timestamp; dates and
|
|
355
|
+
naive timestamps use UTC.
|
|
356
|
+
|
|
357
|
+
`show` prints messages, generated SQL, feedback, then the saved analysis with its
|
|
358
|
+
citations and next steps. Traces are compact by default; `--trace` adds previews
|
|
359
|
+
limited to 500 characters per entry, and `--trace-entry` opens one entry in full.
|
|
360
|
+
The entry identifiers match the analysis's trace references. `--json` returns the
|
|
361
|
+
full messages and traces without those display limits; `--analysis-only --json`
|
|
362
|
+
returns the analysis view. If a retry failed, the previous completed report is
|
|
363
|
+
shown with the failed status. Missing analyses are reported explicitly.
|
|
364
|
+
|
|
365
|
+
Members can read their own chats; administrators can read chats in accessible
|
|
366
|
+
projects. API keys scoped to a project cannot reach another one. `--app-url`
|
|
367
|
+
sets the base URL for browser links when the webapp and API use different hosts
|
|
368
|
+
or ports. JSON list responses include `url` on each item and `next_offset`.
|
|
369
|
+
Exit codes: 0 on a successful read (even with no report or an empty list), 1 for
|
|
370
|
+
an inaccessible/missing chat or trace entry, 2 for invalid arguments, 3 for API
|
|
371
|
+
or authentication errors.
|
|
@@ -1328,3 +1328,56 @@ def get_dbt_observation(
|
|
|
1328
1328
|
except httpx.HTTPError as exc:
|
|
1329
1329
|
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
1330
1330
|
return _dbt_observation_response(response, url)
|
|
1331
|
+
|
|
1332
|
+
|
|
1333
|
+
class ChatNotFoundError(ApiError):
|
|
1334
|
+
"""The conversation does not exist or is not readable by this key."""
|
|
1335
|
+
|
|
1336
|
+
|
|
1337
|
+
def get_chats(
|
|
1338
|
+
*,
|
|
1339
|
+
api_url: str,
|
|
1340
|
+
api_key: str,
|
|
1341
|
+
project_id: str,
|
|
1342
|
+
chat_id: Optional[str] = None,
|
|
1343
|
+
params: Optional[dict[str, Any]] = None,
|
|
1344
|
+
transport: Optional[httpx.BaseTransport] = None,
|
|
1345
|
+
) -> dict[str, Any]:
|
|
1346
|
+
"""Read a conversation page or a conversation with its saved analysis."""
|
|
1347
|
+
url = api_url.rstrip("/") + f"/api/ci/projects/{project_id}/chats"
|
|
1348
|
+
if chat_id:
|
|
1349
|
+
url += f"/{chat_id}"
|
|
1350
|
+
try:
|
|
1351
|
+
with _client(transport=transport) as client:
|
|
1352
|
+
response = client.get(url, params=params, headers={"Authorization": f"Bearer {api_key}"})
|
|
1353
|
+
except httpx.HTTPError as exc:
|
|
1354
|
+
raise ApiError(f"Could not reach the Cassis API at {url}: {exc}") from exc
|
|
1355
|
+
if response.status_code == 401:
|
|
1356
|
+
raise AuthError("The Cassis API rejected the API key (invalid or expired).")
|
|
1357
|
+
if response.status_code == 404 and chat_id and _detail_or_text(response) == "Chat not found":
|
|
1358
|
+
raise ChatNotFoundError("Conversation not found or not accessible to this API key.")
|
|
1359
|
+
if response.status_code in (403, 404):
|
|
1360
|
+
raise ApiError(
|
|
1361
|
+
f"Conversation access denied (HTTP {response.status_code}). Check --project and the API key’s read access and project scope."
|
|
1362
|
+
)
|
|
1363
|
+
if response.status_code >= 400:
|
|
1364
|
+
raise ApiError(f"Cassis API returned HTTP {response.status_code}: {response.text[:500]}")
|
|
1365
|
+
data = _parse_json_response(response, url)
|
|
1366
|
+
valid = isinstance(data, dict)
|
|
1367
|
+
if valid and chat_id:
|
|
1368
|
+
valid = (
|
|
1369
|
+
isinstance(data.get("chat"), dict)
|
|
1370
|
+
and "id" in data["chat"]
|
|
1371
|
+
and isinstance(data.get("messages"), list)
|
|
1372
|
+
and isinstance(data.get("analysis"), dict)
|
|
1373
|
+
)
|
|
1374
|
+
elif valid:
|
|
1375
|
+
valid = (
|
|
1376
|
+
isinstance(data.get("items"), list)
|
|
1377
|
+
and isinstance(data.get("total"), int)
|
|
1378
|
+
and isinstance(data.get("offset"), int)
|
|
1379
|
+
and all(isinstance(row, dict) and "id" in row for row in data["items"])
|
|
1380
|
+
)
|
|
1381
|
+
if not valid:
|
|
1382
|
+
raise ApiError(f"Unexpected response shape from the Cassis API at {url}.")
|
|
1383
|
+
return data
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""Read conversations and saved analyses without running model calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from datetime import datetime, timezone
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Optional
|
|
9
|
+
from urllib.parse import urlencode
|
|
10
|
+
from uuid import UUID
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from cassis_cli.api import DEFAULT_API_URL, ApiError, ChatNotFoundError, get_chats
|
|
14
|
+
from cassis_cli.common import api_failure, one_line, require_api_key, resolve_project_id
|
|
15
|
+
|
|
16
|
+
app = typer.Typer(no_args_is_help=True, help="Read conversations and their saved analyses.")
|
|
17
|
+
_PATH = typer.Option(Path("."), "--path", help="Checkout root containing the context directory.")
|
|
18
|
+
_PROJECT = typer.Option(None, "--project", envvar="CASSIS_PROJECT_ID", help="Project ID; defaults to project.yml.")
|
|
19
|
+
_KEY = typer.Option(None, "--api-key", envvar="CASSIS_API_KEY", help="Cassis API key.")
|
|
20
|
+
_URL = typer.Option(DEFAULT_API_URL, "--api-url", envvar="CASSIS_API_URL", help="Cassis API base URL.")
|
|
21
|
+
_BASE = typer.Option("cassis", "--base-path", envvar="CASSIS_BASE_PATH", help="Context directory within the checkout.")
|
|
22
|
+
_APP_URL = typer.Option(None, "--app-url", help="Webapp base URL for conversation links; defaults to --api-url.")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _url(base: str, project: str, chat: str) -> str:
|
|
26
|
+
return base.rstrip("/") + "/chat?" + urlencode({"projectId": project, "chatId": chat})
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _status(summary: Optional[dict[str, Any]]) -> str:
|
|
30
|
+
if not summary:
|
|
31
|
+
return "Not analyzed"
|
|
32
|
+
if summary.get("status") in ("running", "scheduled", "deferred"):
|
|
33
|
+
return "In progress"
|
|
34
|
+
if summary.get("status") == "completed" and summary.get("problems", 0):
|
|
35
|
+
return "Needs attention"
|
|
36
|
+
if summary.get("status") != "completed" or summary.get("conversation_changed"):
|
|
37
|
+
return "Not analyzed"
|
|
38
|
+
return "Analyzed"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _read(**kwargs: Any) -> dict[str, Any]:
|
|
42
|
+
try:
|
|
43
|
+
return get_chats(**kwargs)
|
|
44
|
+
except ChatNotFoundError as exc:
|
|
45
|
+
typer.echo(str(exc), err=True)
|
|
46
|
+
raise typer.Exit(1) from exc
|
|
47
|
+
except ApiError as exc:
|
|
48
|
+
raise api_failure(exc) from exc
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@app.command("list")
|
|
52
|
+
def list_chats(
|
|
53
|
+
path: Path = _PATH,
|
|
54
|
+
project_id: Optional[str] = _PROJECT,
|
|
55
|
+
api_key: Optional[str] = _KEY,
|
|
56
|
+
api_url: str = _URL,
|
|
57
|
+
base_path: str = _BASE,
|
|
58
|
+
app_url: Optional[str] = _APP_URL,
|
|
59
|
+
limit: int = typer.Option(20, min=1, max=100),
|
|
60
|
+
offset: int = typer.Option(0, min=0),
|
|
61
|
+
since: Optional[str] = typer.Option(None, help="Last-message cutoff: ISO date or timestamp; dates use UTC."),
|
|
62
|
+
feedback: Optional[str] = typer.Option(None, help="Filter: up, down, any or none."),
|
|
63
|
+
review: Optional[str] = typer.Option(None, help="Filter: needs-attention (detected problems only)."),
|
|
64
|
+
query: Optional[str] = typer.Option(None, "--search", help="Search title, author name or email."),
|
|
65
|
+
json_output: bool = typer.Option(False, "--json", help="Print the page as JSON, including links and pagination."),
|
|
66
|
+
) -> None:
|
|
67
|
+
"""List readable chats by latest message. Reads saved results; never starts an analysis."""
|
|
68
|
+
if feedback not in (None, "up", "down", "any", "none"):
|
|
69
|
+
raise typer.BadParameter("Choose up, down, any or none.", param_hint="--feedback")
|
|
70
|
+
if review not in (None, "needs-attention"):
|
|
71
|
+
raise typer.BadParameter("Choose needs-attention.", param_hint="--review")
|
|
72
|
+
if since:
|
|
73
|
+
try:
|
|
74
|
+
dt = datetime.fromisoformat(since.replace("Z", "+00:00"))
|
|
75
|
+
since = (dt if dt.tzinfo else dt.replace(tzinfo=timezone.utc)).isoformat()
|
|
76
|
+
except ValueError as exc:
|
|
77
|
+
raise typer.BadParameter("Use an ISO date or timestamp.", param_hint="--since") from exc
|
|
78
|
+
key = require_api_key(api_key)
|
|
79
|
+
project = resolve_project_id(project_id, path / base_path, quiet=json_output)
|
|
80
|
+
assert project is not None
|
|
81
|
+
params = {
|
|
82
|
+
k: v
|
|
83
|
+
for k, v in dict(limit=limit, offset=offset, since=since, feedback=feedback, review=review, q=query).items()
|
|
84
|
+
if v is not None
|
|
85
|
+
}
|
|
86
|
+
data = _read(api_url=api_url, api_key=key, project_id=project, params=params)
|
|
87
|
+
for row in data["items"]:
|
|
88
|
+
row["url"] = _url(app_url or api_url, project, row["id"])
|
|
89
|
+
data["next_offset"] = (
|
|
90
|
+
data["offset"] + len(data["items"])
|
|
91
|
+
if data["items"] and data["offset"] + len(data["items"]) < data["total"]
|
|
92
|
+
else None
|
|
93
|
+
)
|
|
94
|
+
if json_output:
|
|
95
|
+
typer.echo(json.dumps(data, indent=2, ensure_ascii=False))
|
|
96
|
+
return
|
|
97
|
+
if not data["items"]:
|
|
98
|
+
typer.echo("No conversations match these filters.")
|
|
99
|
+
for row in data["items"]:
|
|
100
|
+
feedback_text = f"up {row.get('feedback_up', 0)} / down {row.get('feedback_down', 0)}"
|
|
101
|
+
typer.echo(
|
|
102
|
+
f"{row['id']} {_status(row.get('analysis_summary'))} {feedback_text} {row.get('last_message_at') or row.get('created_at')} {one_line(row.get('title') or 'Untitled')}"
|
|
103
|
+
)
|
|
104
|
+
typer.echo(f" {row['url']}")
|
|
105
|
+
typer.echo(f"{len(data['items'])} of {data['total']} conversations (offset {data['offset']}).")
|
|
106
|
+
if data["next_offset"] is not None:
|
|
107
|
+
typer.echo(f"Next page: repeat with --offset {data['next_offset']}")
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _analysis(view: dict[str, Any]) -> None:
|
|
111
|
+
typer.echo("\nAnalysis")
|
|
112
|
+
latest = view.get("analysis")
|
|
113
|
+
if latest:
|
|
114
|
+
typer.echo(f"Status: {latest['status']} | Launched: {latest.get('created_at')}")
|
|
115
|
+
if latest.get("error"):
|
|
116
|
+
typer.echo(f"Error: {latest['error']}")
|
|
117
|
+
shown = latest if latest and latest.get("status") == "completed" else view.get("previous_analysis")
|
|
118
|
+
if not shown or not shown.get("result"):
|
|
119
|
+
typer.echo(
|
|
120
|
+
"No completed analysis available." if view.get("enabled") else "Analysis is not enabled for this project."
|
|
121
|
+
)
|
|
122
|
+
return
|
|
123
|
+
if shown is not latest:
|
|
124
|
+
typer.echo(f"Previous completed analysis ({shown['id']}):")
|
|
125
|
+
if shown.get("conversation_changed"):
|
|
126
|
+
typer.echo("New messages are not covered by this analysis.")
|
|
127
|
+
if shown.get("context_changed"):
|
|
128
|
+
typer.echo("Context has changed since this analysis.")
|
|
129
|
+
report = shown["result"]
|
|
130
|
+
typer.echo(report["summary"])
|
|
131
|
+
for index, obs in enumerate(report["observations"], 1):
|
|
132
|
+
typer.echo(f"\n{index}. {obs['title']} [{obs['kind']} / {obs['confidence']}]")
|
|
133
|
+
typer.echo(obs["explanation"])
|
|
134
|
+
typer.echo(f"Next step ({obs['destination']}): {obs['next_step']}")
|
|
135
|
+
for citation in obs["evidence"]:
|
|
136
|
+
typer.echo(f" [{citation['document_id']}] {citation['quote']}")
|
|
137
|
+
for issue in view.get("issues", []):
|
|
138
|
+
typer.echo(f"Linked issue: {issue['id']} — {issue['title']} ({issue['status']})")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@app.command("show")
|
|
142
|
+
def show_chat(
|
|
143
|
+
chat_id: UUID,
|
|
144
|
+
path: Path = _PATH,
|
|
145
|
+
project_id: Optional[str] = _PROJECT,
|
|
146
|
+
api_key: Optional[str] = _KEY,
|
|
147
|
+
api_url: str = _URL,
|
|
148
|
+
base_path: str = _BASE,
|
|
149
|
+
app_url: Optional[str] = _APP_URL,
|
|
150
|
+
trace: bool = typer.Option(False, "--trace", help="Show bounded previews of execution trace entries."),
|
|
151
|
+
trace_entry: Optional[str] = typer.Option(
|
|
152
|
+
None, "--trace-entry", help="Show one full entry: trace:<message-id>:<index>."
|
|
153
|
+
),
|
|
154
|
+
analysis_only: bool = typer.Option(False, "--analysis-only", help="Show only the saved analysis."),
|
|
155
|
+
json_output: bool = typer.Option(False, "--json", help="Print the full conversation, traces and analysis as JSON."),
|
|
156
|
+
) -> None:
|
|
157
|
+
"""Read a conversation followed by its saved analysis. No model calls or automatic retries."""
|
|
158
|
+
if analysis_only and (trace or trace_entry):
|
|
159
|
+
raise typer.BadParameter("--analysis-only cannot be combined with trace options.")
|
|
160
|
+
key = require_api_key(api_key)
|
|
161
|
+
project = resolve_project_id(project_id, path / base_path, quiet=json_output)
|
|
162
|
+
assert project is not None
|
|
163
|
+
data = _read(api_url=api_url, api_key=key, project_id=project, chat_id=str(chat_id))
|
|
164
|
+
data["url"] = _url(app_url or api_url, project, str(chat_id))
|
|
165
|
+
if trace_entry:
|
|
166
|
+
for message in data["messages"]:
|
|
167
|
+
for i, entry in enumerate(message.get("process_log") or []):
|
|
168
|
+
if trace_entry == f"trace:{message['id']}:{i}":
|
|
169
|
+
typer.echo(json.dumps(entry, indent=2, ensure_ascii=False))
|
|
170
|
+
return
|
|
171
|
+
typer.echo("Trace entry not found in this conversation.", err=True)
|
|
172
|
+
raise typer.Exit(1)
|
|
173
|
+
if json_output:
|
|
174
|
+
typer.echo(json.dumps(data["analysis"] if analysis_only else data, indent=2, ensure_ascii=False))
|
|
175
|
+
return
|
|
176
|
+
chat = data["chat"]
|
|
177
|
+
typer.echo(f"{chat.get('title') or 'Untitled'} [{chat['id']}]")
|
|
178
|
+
typer.echo(f"Created: {chat.get('created_at')} | Updated: {chat.get('updated_at')}")
|
|
179
|
+
typer.echo(data["url"])
|
|
180
|
+
if not analysis_only:
|
|
181
|
+
for message in data["messages"]:
|
|
182
|
+
typer.echo(f"\n{message['role']} — {message.get('created_at')} [message:{message['id']}]")
|
|
183
|
+
if message.get("content"):
|
|
184
|
+
typer.echo(message["content"])
|
|
185
|
+
if message.get("generated_sql"):
|
|
186
|
+
typer.echo(f"SQL:\n{message['generated_sql']}")
|
|
187
|
+
if message.get("rating"):
|
|
188
|
+
# A feedback message: its content, printed above, is the justification.
|
|
189
|
+
typer.echo(f"Feedback: {message['rating']}")
|
|
190
|
+
if message.get("error"):
|
|
191
|
+
typer.echo(f"Error: {message['error']}")
|
|
192
|
+
entries = message.get("process_log") or []
|
|
193
|
+
if entries and not trace:
|
|
194
|
+
typer.echo(f"Execution: {len(entries)} trace entries (use --trace to inspect).")
|
|
195
|
+
if trace:
|
|
196
|
+
for i, entry in enumerate(entries):
|
|
197
|
+
ref = f"trace:{message['id']}:{i}"
|
|
198
|
+
typer.echo(f" [{ref}] {entry.get('name') or entry.get('type')}")
|
|
199
|
+
detail = json.dumps(entry, ensure_ascii=False)
|
|
200
|
+
typer.echo(
|
|
201
|
+
f" {detail[:500]}"
|
|
202
|
+
+ (f"… (use --trace-entry {ref} for full details)" if len(detail) > 500 else "")
|
|
203
|
+
)
|
|
204
|
+
_analysis(data["analysis"])
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import typer
|
|
6
6
|
from cassis_cli import __version__
|
|
7
|
+
from cassis_cli.chats import app as chats_app
|
|
7
8
|
from cassis_cli.dbt import app as dbt_app
|
|
8
9
|
from cassis_cli.eval import app as eval_app
|
|
9
10
|
from cassis_cli.issues import app as issues_app
|
|
@@ -29,6 +30,7 @@ app.add_typer(schema_app, name="schema")
|
|
|
29
30
|
app.add_typer(dbt_app, name="dbt")
|
|
30
31
|
app.add_typer(projects_app, name="projects")
|
|
31
32
|
app.add_typer(issues_app, name="issues")
|
|
33
|
+
app.add_typer(chats_app, name="chats")
|
|
32
34
|
app.command()(status)
|
|
33
35
|
app.command()(verify)
|
|
34
36
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|