artificial-intelligence-usage 0.0.4__tar.gz → 0.0.5__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.
- {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/PKG-INFO +17 -2
- {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/README.md +15 -1
- {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/pyproject.toml +2 -1
- {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/src/artificial_intelligence_usage.egg-info/PKG-INFO +17 -2
- {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/src/artificial_intelligence_usage.egg-info/SOURCES.txt +1 -0
- artificial_intelligence_usage-0.0.5/src/artificial_intelligence_usage.egg-info/requires.txt +1 -0
- {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/src/artificial_intelligence_usage.py +307 -9
- artificial_intelligence_usage-0.0.5/tests/test_artificial_intelligence_usage.py +608 -0
- artificial_intelligence_usage-0.0.4/tests/test_artificial_intelligence_usage.py +0 -260
- {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/setup.cfg +0 -0
- {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/src/artificial_intelligence_usage.egg-info/dependency_links.txt +0 -0
- {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/src/artificial_intelligence_usage.egg-info/entry_points.txt +0 -0
- {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/src/artificial_intelligence_usage.egg-info/top_level.txt +0 -0
- {artificial_intelligence_usage-0.0.4 → artificial_intelligence_usage-0.0.5}/tests/test_publish_pypi.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: artificial-intelligence-usage
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: Analyze usage of artificial intelligence tool.
|
|
5
5
|
Author: Kaizten Analytics
|
|
6
6
|
License-Expression: LicenseRef-Proprietary
|
|
@@ -8,11 +8,12 @@ Classifier: Programming Language :: Python :: 3
|
|
|
8
8
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
9
9
|
Requires-Python: >=3.10
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: langid==1.1.6
|
|
11
12
|
|
|
12
13
|
# Artificial Intelligence Usage
|
|
13
14
|
|
|
14
15
|
`artificial-intelligence-usage` analyzes local Codex session rollouts and prints a summary of the
|
|
15
|
-
latest session followed by daily session
|
|
16
|
+
latest session followed by daily and per-session prompt, token, and rate-limit usage.
|
|
16
17
|
|
|
17
18
|
## Behavior
|
|
18
19
|
|
|
@@ -22,6 +23,19 @@ latest session followed by daily session, turn, token, and rate-limit usage.
|
|
|
22
23
|
- Shows status details from the most recently modified session.
|
|
23
24
|
- Reports sessions from the last 14 calendar days by default, including today.
|
|
24
25
|
- Counts user turns and cumulative tokens for each session.
|
|
26
|
+
- Breaks applied prompts down into Normal, Plan, and Goal counts. Goal counts
|
|
27
|
+
include automatic goal continuations, while background tasks such as guardian
|
|
28
|
+
reviews are excluded.
|
|
29
|
+
- Shows a second table with one row per session, including its start time,
|
|
30
|
+
session name (up to 10 characters), first-prompt preview (up to 20
|
|
31
|
+
characters), GitHub repository, usage, and language.
|
|
32
|
+
- Reads session names from `~/.codex/session_index.jsonl`. Missing names and
|
|
33
|
+
prompts display as `Unknown`.
|
|
34
|
+
- For goal-only sessions, reads the goal objective from
|
|
35
|
+
`~/.codex/goals_1.sqlite` for the prompt preview and reports its language as
|
|
36
|
+
`Unknown`.
|
|
37
|
+
- Uses `langid`, restricted to English and Spanish, for prompt language
|
|
38
|
+
detection. Sessions without a detectable user prompt show `Unknown`.
|
|
25
39
|
- Shows the remaining five-hour and weekly allowance recorded in each day's
|
|
26
40
|
last session.
|
|
27
41
|
- Ignores malformed JSON lines and incomplete status events.
|
|
@@ -31,6 +45,7 @@ The tool only reads local session files and does not modify them.
|
|
|
31
45
|
## Requirements
|
|
32
46
|
|
|
33
47
|
- Python 3.10 or newer
|
|
48
|
+
- `langid` 1.1.6 (installed automatically with the package)
|
|
34
49
|
|
|
35
50
|
## Building
|
|
36
51
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Artificial Intelligence Usage
|
|
2
2
|
|
|
3
3
|
`artificial-intelligence-usage` analyzes local Codex session rollouts and prints a summary of the
|
|
4
|
-
latest session followed by daily session
|
|
4
|
+
latest session followed by daily and per-session prompt, token, and rate-limit usage.
|
|
5
5
|
|
|
6
6
|
## Behavior
|
|
7
7
|
|
|
@@ -11,6 +11,19 @@ latest session followed by daily session, turn, token, and rate-limit usage.
|
|
|
11
11
|
- Shows status details from the most recently modified session.
|
|
12
12
|
- Reports sessions from the last 14 calendar days by default, including today.
|
|
13
13
|
- Counts user turns and cumulative tokens for each session.
|
|
14
|
+
- Breaks applied prompts down into Normal, Plan, and Goal counts. Goal counts
|
|
15
|
+
include automatic goal continuations, while background tasks such as guardian
|
|
16
|
+
reviews are excluded.
|
|
17
|
+
- Shows a second table with one row per session, including its start time,
|
|
18
|
+
session name (up to 10 characters), first-prompt preview (up to 20
|
|
19
|
+
characters), GitHub repository, usage, and language.
|
|
20
|
+
- Reads session names from `~/.codex/session_index.jsonl`. Missing names and
|
|
21
|
+
prompts display as `Unknown`.
|
|
22
|
+
- For goal-only sessions, reads the goal objective from
|
|
23
|
+
`~/.codex/goals_1.sqlite` for the prompt preview and reports its language as
|
|
24
|
+
`Unknown`.
|
|
25
|
+
- Uses `langid`, restricted to English and Spanish, for prompt language
|
|
26
|
+
detection. Sessions without a detectable user prompt show `Unknown`.
|
|
14
27
|
- Shows the remaining five-hour and weekly allowance recorded in each day's
|
|
15
28
|
last session.
|
|
16
29
|
- Ignores malformed JSON lines and incomplete status events.
|
|
@@ -20,6 +33,7 @@ The tool only reads local session files and does not modify them.
|
|
|
20
33
|
## Requirements
|
|
21
34
|
|
|
22
35
|
- Python 3.10 or newer
|
|
36
|
+
- `langid` 1.1.6 (installed automatically with the package)
|
|
23
37
|
|
|
24
38
|
## Building
|
|
25
39
|
|
|
@@ -4,12 +4,13 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "artificial-intelligence-usage"
|
|
7
|
-
version = "0.0.
|
|
7
|
+
version = "0.0.5"
|
|
8
8
|
description = "Analyze usage of artificial intelligence tool."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
11
11
|
license = "LicenseRef-Proprietary"
|
|
12
12
|
authors = [{ name = "Kaizten Analytics" }]
|
|
13
|
+
dependencies = ["langid==1.1.6"]
|
|
13
14
|
classifiers = [
|
|
14
15
|
"Programming Language :: Python :: 3",
|
|
15
16
|
"Programming Language :: Python :: 3 :: Only",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: artificial-intelligence-usage
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.5
|
|
4
4
|
Summary: Analyze usage of artificial intelligence tool.
|
|
5
5
|
Author: Kaizten Analytics
|
|
6
6
|
License-Expression: LicenseRef-Proprietary
|
|
@@ -8,11 +8,12 @@ Classifier: Programming Language :: Python :: 3
|
|
|
8
8
|
Classifier: Programming Language :: Python :: 3 :: Only
|
|
9
9
|
Requires-Python: >=3.10
|
|
10
10
|
Description-Content-Type: text/markdown
|
|
11
|
+
Requires-Dist: langid==1.1.6
|
|
11
12
|
|
|
12
13
|
# Artificial Intelligence Usage
|
|
13
14
|
|
|
14
15
|
`artificial-intelligence-usage` analyzes local Codex session rollouts and prints a summary of the
|
|
15
|
-
latest session followed by daily session
|
|
16
|
+
latest session followed by daily and per-session prompt, token, and rate-limit usage.
|
|
16
17
|
|
|
17
18
|
## Behavior
|
|
18
19
|
|
|
@@ -22,6 +23,19 @@ latest session followed by daily session, turn, token, and rate-limit usage.
|
|
|
22
23
|
- Shows status details from the most recently modified session.
|
|
23
24
|
- Reports sessions from the last 14 calendar days by default, including today.
|
|
24
25
|
- Counts user turns and cumulative tokens for each session.
|
|
26
|
+
- Breaks applied prompts down into Normal, Plan, and Goal counts. Goal counts
|
|
27
|
+
include automatic goal continuations, while background tasks such as guardian
|
|
28
|
+
reviews are excluded.
|
|
29
|
+
- Shows a second table with one row per session, including its start time,
|
|
30
|
+
session name (up to 10 characters), first-prompt preview (up to 20
|
|
31
|
+
characters), GitHub repository, usage, and language.
|
|
32
|
+
- Reads session names from `~/.codex/session_index.jsonl`. Missing names and
|
|
33
|
+
prompts display as `Unknown`.
|
|
34
|
+
- For goal-only sessions, reads the goal objective from
|
|
35
|
+
`~/.codex/goals_1.sqlite` for the prompt preview and reports its language as
|
|
36
|
+
`Unknown`.
|
|
37
|
+
- Uses `langid`, restricted to English and Spanish, for prompt language
|
|
38
|
+
detection. Sessions without a detectable user prompt show `Unknown`.
|
|
25
39
|
- Shows the remaining five-hour and weekly allowance recorded in each day's
|
|
26
40
|
last session.
|
|
27
41
|
- Ignores malformed JSON lines and incomplete status events.
|
|
@@ -31,6 +45,7 @@ The tool only reads local session files and does not modify them.
|
|
|
31
45
|
## Requirements
|
|
32
46
|
|
|
33
47
|
- Python 3.10 or newer
|
|
48
|
+
- `langid` 1.1.6 (installed automatically with the package)
|
|
34
49
|
|
|
35
50
|
## Building
|
|
36
51
|
|
|
@@ -5,6 +5,7 @@ src/artificial_intelligence_usage.egg-info/PKG-INFO
|
|
|
5
5
|
src/artificial_intelligence_usage.egg-info/SOURCES.txt
|
|
6
6
|
src/artificial_intelligence_usage.egg-info/dependency_links.txt
|
|
7
7
|
src/artificial_intelligence_usage.egg-info/entry_points.txt
|
|
8
|
+
src/artificial_intelligence_usage.egg-info/requires.txt
|
|
8
9
|
src/artificial_intelligence_usage.egg-info/top_level.txt
|
|
9
10
|
tests/test_artificial_intelligence_usage.py
|
|
10
11
|
tests/test_publish_pypi.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
langid==1.1.6
|
|
@@ -5,16 +5,46 @@ from __future__ import annotations
|
|
|
5
5
|
|
|
6
6
|
import argparse
|
|
7
7
|
import json
|
|
8
|
+
import re
|
|
8
9
|
import shutil
|
|
10
|
+
import sqlite3
|
|
9
11
|
import subprocess
|
|
10
12
|
from collections import Counter, defaultdict
|
|
13
|
+
from dataclasses import dataclass
|
|
11
14
|
from datetime import date, datetime, timedelta
|
|
12
15
|
from pathlib import Path
|
|
13
16
|
from typing import Any
|
|
14
17
|
|
|
18
|
+
import langid
|
|
19
|
+
|
|
15
20
|
|
|
16
21
|
DEFAULT_DAYS = 14
|
|
17
22
|
SUPPORTED_TOOLS = (("Codex", "codex"), ("Claude", "claude"))
|
|
23
|
+
INTERNAL_USER_TEXT_TAGS = {
|
|
24
|
+
"codex_internal_context",
|
|
25
|
+
"environment_context",
|
|
26
|
+
"goal",
|
|
27
|
+
"goals",
|
|
28
|
+
"guardian_tool_descriptions",
|
|
29
|
+
"turn_aborted",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
langid.set_languages(["en", "es"])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class SessionAnalysis:
|
|
37
|
+
"""Usage information extracted from one Codex session."""
|
|
38
|
+
|
|
39
|
+
turns: int
|
|
40
|
+
tokens: int
|
|
41
|
+
normal_prompts: int
|
|
42
|
+
plan_prompts: int
|
|
43
|
+
goal_prompts: int
|
|
44
|
+
repository: str
|
|
45
|
+
language: str
|
|
46
|
+
session_id: str | None
|
|
47
|
+
first_prompt: str | None
|
|
18
48
|
|
|
19
49
|
|
|
20
50
|
def positive_int(value: str) -> int:
|
|
@@ -263,10 +293,170 @@ def print_status(session_file: Path | None) -> None:
|
|
|
263
293
|
print()
|
|
264
294
|
|
|
265
295
|
|
|
266
|
-
def
|
|
267
|
-
"""Return
|
|
296
|
+
def prompt_type(payload: dict[str, Any]) -> str | None:
|
|
297
|
+
"""Return the prompt type for a task-started payload, if applicable."""
|
|
298
|
+
attribution = payload.get("turn_attribution")
|
|
299
|
+
if isinstance(attribution, dict):
|
|
300
|
+
trigger = attribution.get("turn_trigger")
|
|
301
|
+
elif isinstance(attribution, str):
|
|
302
|
+
trigger = attribution
|
|
303
|
+
else:
|
|
304
|
+
trigger = None
|
|
305
|
+
|
|
306
|
+
if trigger == "goal":
|
|
307
|
+
return "goal"
|
|
308
|
+
|
|
309
|
+
if trigger not in (None, "user"):
|
|
310
|
+
return None
|
|
311
|
+
|
|
312
|
+
mode = payload.get("collaboration_mode_kind")
|
|
313
|
+
if mode == "plan":
|
|
314
|
+
return "plan"
|
|
315
|
+
if mode in (None, "default"):
|
|
316
|
+
return "normal"
|
|
317
|
+
|
|
318
|
+
return None
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def github_repository(repository_url: Any) -> str:
|
|
322
|
+
"""Return ``owner/repository`` for a GitHub remote URL."""
|
|
323
|
+
if not isinstance(repository_url, str):
|
|
324
|
+
return "Unknown"
|
|
325
|
+
|
|
326
|
+
match = re.match(
|
|
327
|
+
r"^(?:https?://|ssh://git@|git@)github\.com[/:]"
|
|
328
|
+
r"(?P<owner>[^/]+)/(?P<repository>[^/#]+)/?$",
|
|
329
|
+
repository_url.strip(),
|
|
330
|
+
re.IGNORECASE,
|
|
331
|
+
)
|
|
332
|
+
if match is None:
|
|
333
|
+
return "Unknown"
|
|
334
|
+
|
|
335
|
+
repository = match.group("repository").removesuffix(".git")
|
|
336
|
+
return f"{match.group('owner')}/{repository}"
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def is_internal_user_text(text: str) -> bool:
|
|
340
|
+
"""Return whether user-role text contains Codex-injected context."""
|
|
341
|
+
match = re.match(r"^\s*<([A-Za-z0-9_-]+)(?:\s|>)", text)
|
|
342
|
+
return match is not None and match.group(1) in INTERNAL_USER_TEXT_TAGS
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def user_prompt_text(payload: dict[str, Any]) -> str | None:
|
|
346
|
+
"""Return real user-authored text from a user-message payload."""
|
|
347
|
+
content = payload.get("content")
|
|
348
|
+
if not isinstance(content, list):
|
|
349
|
+
return None
|
|
350
|
+
|
|
351
|
+
parts = []
|
|
352
|
+
for item in content:
|
|
353
|
+
if not isinstance(item, dict) or item.get("type") != "input_text":
|
|
354
|
+
continue
|
|
355
|
+
|
|
356
|
+
text = item.get("text")
|
|
357
|
+
if (
|
|
358
|
+
isinstance(text, str)
|
|
359
|
+
and text.strip()
|
|
360
|
+
and not is_internal_user_text(text)
|
|
361
|
+
):
|
|
362
|
+
parts.append(text.strip())
|
|
363
|
+
|
|
364
|
+
return "\n".join(parts) or None
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def detect_language(prompt: str | None) -> str:
|
|
368
|
+
"""Classify a prompt as English or Spanish."""
|
|
369
|
+
if prompt is None:
|
|
370
|
+
return "Unknown"
|
|
371
|
+
|
|
372
|
+
language_code, _confidence = langid.classify(prompt)
|
|
373
|
+
return {"en": "English", "es": "Spanish"}.get(language_code, "Unknown")
|
|
374
|
+
|
|
375
|
+
|
|
376
|
+
def compact_text(value: str | None, maximum_length: int) -> str:
|
|
377
|
+
"""Normalize whitespace and truncate display text."""
|
|
378
|
+
if not isinstance(value, str):
|
|
379
|
+
return "Unknown"
|
|
380
|
+
|
|
381
|
+
normalized = " ".join(value.split())
|
|
382
|
+
return normalized[:maximum_length] or "Unknown"
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def read_session_names(session_index_file: Path) -> dict[str, str]:
|
|
386
|
+
"""Return the latest indexed name for each Codex session."""
|
|
387
|
+
names: dict[str, str] = {}
|
|
388
|
+
|
|
389
|
+
try:
|
|
390
|
+
with session_index_file.open("r", encoding="utf-8") as file_handle:
|
|
391
|
+
for line in file_handle:
|
|
392
|
+
try:
|
|
393
|
+
entry = json.loads(line)
|
|
394
|
+
except (json.JSONDecodeError, UnicodeDecodeError):
|
|
395
|
+
continue
|
|
396
|
+
|
|
397
|
+
session_id = entry.get("id")
|
|
398
|
+
thread_name = entry.get("thread_name")
|
|
399
|
+
if (
|
|
400
|
+
isinstance(session_id, str)
|
|
401
|
+
and session_id
|
|
402
|
+
and isinstance(thread_name, str)
|
|
403
|
+
and thread_name.strip()
|
|
404
|
+
):
|
|
405
|
+
names[session_id] = thread_name
|
|
406
|
+
except (OSError, UnicodeError):
|
|
407
|
+
pass
|
|
408
|
+
|
|
409
|
+
return names
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def read_goal_objectives(goals_database: Path) -> dict[str, str]:
|
|
413
|
+
"""Return goal objectives keyed by their Codex thread ID."""
|
|
414
|
+
if not goals_database.is_file():
|
|
415
|
+
return {}
|
|
416
|
+
|
|
417
|
+
connection: sqlite3.Connection | None = None
|
|
418
|
+
try:
|
|
419
|
+
database_uri = f"{goals_database.resolve().as_uri()}?mode=ro"
|
|
420
|
+
connection = sqlite3.connect(database_uri, uri=True)
|
|
421
|
+
rows = connection.execute(
|
|
422
|
+
"SELECT thread_id, objective FROM thread_goals"
|
|
423
|
+
).fetchall()
|
|
424
|
+
except (OSError, sqlite3.Error):
|
|
425
|
+
return {}
|
|
426
|
+
finally:
|
|
427
|
+
if connection is not None:
|
|
428
|
+
connection.close()
|
|
429
|
+
|
|
430
|
+
return {
|
|
431
|
+
thread_id: objective
|
|
432
|
+
for thread_id, objective in rows
|
|
433
|
+
if isinstance(thread_id, str)
|
|
434
|
+
and thread_id
|
|
435
|
+
and isinstance(objective, str)
|
|
436
|
+
and objective.strip()
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
def session_start_time(session_file: Path) -> str:
|
|
441
|
+
"""Return the session start time encoded in a rollout filename."""
|
|
442
|
+
match = re.match(
|
|
443
|
+
r"^rollout-\d{4}-\d{2}-\d{2}T(\d{2})-(\d{2})-(\d{2})-",
|
|
444
|
+
session_file.name,
|
|
445
|
+
)
|
|
446
|
+
if match is None:
|
|
447
|
+
return "Unknown"
|
|
448
|
+
|
|
449
|
+
return ":".join(match.groups())
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def analyze_session(session_file: Path) -> SessionAnalysis:
|
|
453
|
+
"""Return usage information extracted from one Codex session."""
|
|
268
454
|
turns = 0
|
|
269
455
|
total_tokens = 0
|
|
456
|
+
prompt_counts: Counter[str] = Counter()
|
|
457
|
+
repository = "Unknown"
|
|
458
|
+
session_id = None
|
|
459
|
+
first_prompt = None
|
|
270
460
|
|
|
271
461
|
with session_file.open("r", encoding="utf-8") as file_handle:
|
|
272
462
|
for line in file_handle:
|
|
@@ -277,12 +467,29 @@ def analyze_session(session_file: Path) -> tuple[int, int]:
|
|
|
277
467
|
|
|
278
468
|
payload = event.get("payload") or {}
|
|
279
469
|
|
|
470
|
+
if event.get("type") == "session_meta":
|
|
471
|
+
candidate_session_id = payload.get("id") or payload.get("session_id")
|
|
472
|
+
if isinstance(candidate_session_id, str) and candidate_session_id:
|
|
473
|
+
session_id = candidate_session_id
|
|
474
|
+
git = payload.get("git") or {}
|
|
475
|
+
repository = github_repository(git.get("repository_url"))
|
|
476
|
+
|
|
477
|
+
if (
|
|
478
|
+
event.get("type") == "event_msg"
|
|
479
|
+
and payload.get("type") == "task_started"
|
|
480
|
+
):
|
|
481
|
+
category = prompt_type(payload)
|
|
482
|
+
if category is not None:
|
|
483
|
+
prompt_counts[category] += 1
|
|
484
|
+
|
|
280
485
|
if (
|
|
281
486
|
event.get("type") == "response_item"
|
|
282
487
|
and payload.get("type") == "message"
|
|
283
488
|
and payload.get("role") == "user"
|
|
284
489
|
):
|
|
285
490
|
turns += 1
|
|
491
|
+
if first_prompt is None:
|
|
492
|
+
first_prompt = user_prompt_text(payload)
|
|
286
493
|
|
|
287
494
|
if (
|
|
288
495
|
event.get("type") == "event_msg"
|
|
@@ -295,7 +502,17 @@ def analyze_session(session_file: Path) -> tuple[int, int]:
|
|
|
295
502
|
if isinstance(value, (int, float)):
|
|
296
503
|
total_tokens = max(total_tokens, int(value))
|
|
297
504
|
|
|
298
|
-
return
|
|
505
|
+
return SessionAnalysis(
|
|
506
|
+
turns=turns,
|
|
507
|
+
tokens=total_tokens,
|
|
508
|
+
normal_prompts=prompt_counts["normal"],
|
|
509
|
+
plan_prompts=prompt_counts["plan"],
|
|
510
|
+
goal_prompts=prompt_counts["goal"],
|
|
511
|
+
repository=repository,
|
|
512
|
+
language=detect_language(first_prompt),
|
|
513
|
+
session_id=session_id,
|
|
514
|
+
first_prompt=first_prompt,
|
|
515
|
+
)
|
|
299
516
|
|
|
300
517
|
|
|
301
518
|
def build_parser() -> argparse.ArgumentParser:
|
|
@@ -315,6 +532,8 @@ def run_report(
|
|
|
315
532
|
days: int,
|
|
316
533
|
today: date | None = None,
|
|
317
534
|
tool_versions: dict[str, str] | None = None,
|
|
535
|
+
session_index_file: Path | None = None,
|
|
536
|
+
goals_database: Path | None = None,
|
|
318
537
|
) -> int:
|
|
319
538
|
"""Analyze sessions and print the status and daily usage report."""
|
|
320
539
|
current_day = today or date.today()
|
|
@@ -322,12 +541,24 @@ def run_report(
|
|
|
322
541
|
session_counts: Counter[str] = Counter()
|
|
323
542
|
turn_counts: defaultdict[str, list[int]] = defaultdict(list)
|
|
324
543
|
token_counts: Counter[str] = Counter()
|
|
544
|
+
normal_prompt_counts: Counter[str] = Counter()
|
|
545
|
+
plan_prompt_counts: Counter[str] = Counter()
|
|
546
|
+
goal_prompt_counts: Counter[str] = Counter()
|
|
325
547
|
last_session_by_date: dict[str, Path] = {}
|
|
548
|
+
session_rows: list[tuple[str, Path, SessionAnalysis]] = []
|
|
326
549
|
|
|
327
550
|
if tool_versions is None:
|
|
328
551
|
tool_versions = detect_installed_tool_versions()
|
|
329
552
|
print_installed_tool_versions(tool_versions)
|
|
330
553
|
|
|
554
|
+
codex_dir = sessions_dir.parent
|
|
555
|
+
session_names = read_session_names(
|
|
556
|
+
session_index_file or codex_dir / "session_index.jsonl"
|
|
557
|
+
)
|
|
558
|
+
goal_objectives = read_goal_objectives(
|
|
559
|
+
goals_database or codex_dir / "goals_1.sqlite"
|
|
560
|
+
)
|
|
561
|
+
|
|
331
562
|
all_session_files = list(sessions_dir.glob("*/*/*/rollout-*.jsonl"))
|
|
332
563
|
latest_session = (
|
|
333
564
|
max(all_session_files, key=lambda path: path.stat().st_mtime)
|
|
@@ -349,10 +580,14 @@ def run_report(
|
|
|
349
580
|
if not first_day <= session_date <= current_day:
|
|
350
581
|
continue
|
|
351
582
|
|
|
352
|
-
|
|
583
|
+
analysis = analyze_session(session_file)
|
|
353
584
|
session_counts[date_label] += 1
|
|
354
|
-
turn_counts[date_label].append(turns)
|
|
355
|
-
token_counts[date_label] += tokens
|
|
585
|
+
turn_counts[date_label].append(analysis.turns)
|
|
586
|
+
token_counts[date_label] += analysis.tokens
|
|
587
|
+
normal_prompt_counts[date_label] += analysis.normal_prompts
|
|
588
|
+
plan_prompt_counts[date_label] += analysis.plan_prompts
|
|
589
|
+
goal_prompt_counts[date_label] += analysis.goal_prompts
|
|
590
|
+
session_rows.append((date_label, session_file, analysis))
|
|
356
591
|
|
|
357
592
|
previous_last = last_session_by_date.get(date_label)
|
|
358
593
|
if previous_last is None or session_file.name > previous_last.name:
|
|
@@ -362,16 +597,20 @@ def run_report(
|
|
|
362
597
|
print(f"No Codex sessions found in the last {days} days in {sessions_dir}")
|
|
363
598
|
return 0
|
|
364
599
|
|
|
365
|
-
|
|
600
|
+
header = (
|
|
366
601
|
f"{'Date':<12} "
|
|
367
602
|
f"{'Sessions':>8} "
|
|
368
603
|
f"{'Turns':>8} "
|
|
369
604
|
f"{'Avg turns':>10} "
|
|
605
|
+
f"{'Normal':>8} "
|
|
606
|
+
f"{'Plan':>8} "
|
|
607
|
+
f"{'Goal':>8} "
|
|
370
608
|
f"{'Tokens':>15} "
|
|
371
609
|
f"{'5h last session':>17} "
|
|
372
610
|
f"{'Weekly last session':>20}"
|
|
373
611
|
)
|
|
374
|
-
print(
|
|
612
|
+
print(header)
|
|
613
|
+
print("-" * len(header))
|
|
375
614
|
|
|
376
615
|
for date_label in sorted(session_counts):
|
|
377
616
|
sessions = session_counts[date_label]
|
|
@@ -385,6 +624,9 @@ def run_report(
|
|
|
385
624
|
f"{sessions:>8} "
|
|
386
625
|
f"{total_turns:>8} "
|
|
387
626
|
f"{avg_turns:>10.1f} "
|
|
627
|
+
f"{normal_prompt_counts[date_label]:>8} "
|
|
628
|
+
f"{plan_prompt_counts[date_label]:>8} "
|
|
629
|
+
f"{goal_prompt_counts[date_label]:>8} "
|
|
388
630
|
f"{tokens:>15,} "
|
|
389
631
|
f"{five_hour:>17} "
|
|
390
632
|
f"{weekly:>20}"
|
|
@@ -393,18 +635,74 @@ def run_report(
|
|
|
393
635
|
total_sessions = sum(session_counts.values())
|
|
394
636
|
total_turns = sum(sum(turns) for turns in turn_counts.values())
|
|
395
637
|
total_tokens = sum(token_counts.values())
|
|
638
|
+
total_normal_prompts = sum(normal_prompt_counts.values())
|
|
639
|
+
total_plan_prompts = sum(plan_prompt_counts.values())
|
|
640
|
+
total_goal_prompts = sum(goal_prompt_counts.values())
|
|
396
641
|
overall_average = total_turns / total_sessions
|
|
397
642
|
|
|
398
|
-
print("-" *
|
|
643
|
+
print("-" * len(header))
|
|
399
644
|
print(
|
|
400
645
|
f"{'Total':<12} "
|
|
401
646
|
f"{total_sessions:>8} "
|
|
402
647
|
f"{total_turns:>8} "
|
|
403
648
|
f"{overall_average:>10.1f} "
|
|
649
|
+
f"{total_normal_prompts:>8} "
|
|
650
|
+
f"{total_plan_prompts:>8} "
|
|
651
|
+
f"{total_goal_prompts:>8} "
|
|
404
652
|
f"{total_tokens:>15,} "
|
|
405
653
|
f"{'-':>17} "
|
|
406
654
|
f"{'-':>20}"
|
|
407
655
|
)
|
|
656
|
+
|
|
657
|
+
print()
|
|
658
|
+
print("Sessions")
|
|
659
|
+
repository_width = max(
|
|
660
|
+
len("Repository"),
|
|
661
|
+
*(len(analysis.repository) for _date, _path, analysis in session_rows),
|
|
662
|
+
)
|
|
663
|
+
session_header = (
|
|
664
|
+
f"{'Date':<12} "
|
|
665
|
+
f"{'Start':<8} "
|
|
666
|
+
f"{'Repository':<{repository_width}} "
|
|
667
|
+
f"{'Name':<10} "
|
|
668
|
+
f"{'First prompt':<20} "
|
|
669
|
+
f"{'Turns':>8} "
|
|
670
|
+
f"{'Normal':>8} "
|
|
671
|
+
f"{'Plan':>8} "
|
|
672
|
+
f"{'Goal':>8} "
|
|
673
|
+
f"{'Language':<8} "
|
|
674
|
+
f"{'Tokens':>15} "
|
|
675
|
+
f"{'5h last session':>17} "
|
|
676
|
+
f"{'Weekly last session':>20}"
|
|
677
|
+
)
|
|
678
|
+
print(session_header)
|
|
679
|
+
print("-" * len(session_header))
|
|
680
|
+
|
|
681
|
+
for date_label, session_file, analysis in sorted(
|
|
682
|
+
session_rows, key=lambda row: (row[0], row[1].name)
|
|
683
|
+
):
|
|
684
|
+
five_hour, weekly = session_rate_limits(session_file)
|
|
685
|
+
session_name = compact_text(session_names.get(analysis.session_id), 10)
|
|
686
|
+
first_prompt = analysis.first_prompt
|
|
687
|
+
if first_prompt is None and analysis.goal_prompts:
|
|
688
|
+
first_prompt = goal_objectives.get(analysis.session_id)
|
|
689
|
+
prompt_preview = compact_text(first_prompt, 20)
|
|
690
|
+
print(
|
|
691
|
+
f"{date_label:<12} "
|
|
692
|
+
f"{session_start_time(session_file):<8} "
|
|
693
|
+
f"{analysis.repository:<{repository_width}} "
|
|
694
|
+
f"{session_name:<10} "
|
|
695
|
+
f"{prompt_preview:<20} "
|
|
696
|
+
f"{analysis.turns:>8} "
|
|
697
|
+
f"{analysis.normal_prompts:>8} "
|
|
698
|
+
f"{analysis.plan_prompts:>8} "
|
|
699
|
+
f"{analysis.goal_prompts:>8} "
|
|
700
|
+
f"{analysis.language:<8} "
|
|
701
|
+
f"{analysis.tokens:>15,} "
|
|
702
|
+
f"{five_hour:>17} "
|
|
703
|
+
f"{weekly:>20}"
|
|
704
|
+
)
|
|
705
|
+
|
|
408
706
|
return 0
|
|
409
707
|
|
|
410
708
|
|