reprompt-cli 1.7.1__tar.gz → 1.8.1__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.
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/PKG-INFO +3 -2
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/README.md +2 -1
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/pyproject.toml +1 -1
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/__init__.py +1 -1
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/cli.py +110 -1
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/insights.py +26 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/pipeline.py +99 -0
- reprompt_cli-1.8.1/src/reprompt/core/repetition.py +128 -0
- reprompt_cli-1.8.1/src/reprompt/core/session_quality.py +201 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/suggestions.py +6 -0
- reprompt_cli-1.8.1/src/reprompt/output/repetition_terminal.py +61 -0
- reprompt_cli-1.8.1/src/reprompt/output/sessions_terminal.py +166 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/storage/db.py +123 -1
- reprompt_cli-1.8.1/tests/test_db_session_quality.py +181 -0
- reprompt_cli-1.8.1/tests/test_repetition.py +124 -0
- reprompt_cli-1.8.1/tests/test_repetition_cli.py +102 -0
- reprompt_cli-1.8.1/tests/test_repetition_output.py +76 -0
- reprompt_cli-1.8.1/tests/test_session_quality.py +455 -0
- reprompt_cli-1.8.1/tests/test_sessions_cli.py +193 -0
- reprompt_cli-1.8.1/tests/test_sessions_output.py +102 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_suggestions.py +12 -1
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/uv.lock +1 -1
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.editorconfig +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.github/dependabot.yml +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.github/workflows/ci.yml +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.github/workflows/publish.yml +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.gitignore +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.pre-commit-config.yaml +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.pre-commit-hooks.yaml +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.testmondata-shm +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/.testmondata-wal +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/CHANGELOG.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/CODE_OF_CONDUCT.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/CONTRIBUTING.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/LICENSE +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/SECURITY.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/Screenshot 2026-03-24 at 09.45.03.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/action.yml +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/demo.gif +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/brand-icon-128.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/brand-icon-16.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/brand-icon-256.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/brand-icon-32.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/brand-icon-48.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/brand-icon-512.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/brand-icon-96.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/brand-icon.svg +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-128.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-16.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-256.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-32.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-48.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-512.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-favicon-96.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-favicon.svg +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-icon-128.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-icon-16.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-icon-256.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-icon-32.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-icon-48.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-icon-512.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-icon-96.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/cli-icon.svg +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/favicon-128.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/favicon-16.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/favicon-256.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/favicon-32.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/favicon-48.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/favicon-512.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/favicon-96.png +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/favicon.svg +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/icons/generate.sh +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/superpowers/specs/2026-03-24-v14-command-consolidation-design.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/docs/superpowers/specs/2026-03-25-v1.5-dashboard-design.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/scripts/generate_demo_data.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/aider.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/base.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/chatgpt.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/claude_chat.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/claude_code.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/cline.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/codex.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/cursor.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/filters.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/gemini.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/adapters/openclaw.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/bridge/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/bridge/handler.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/bridge/host.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/bridge/manifest.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/bridge/protocol.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/commands/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/commands/telemetry.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/commands/wrapped.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/config.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/agent.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/analyzer.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/compress.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/conversation.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/cost.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/dashboard.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/dedup.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/digest.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/distill.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/effectiveness.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/extractors.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/extractors_zh.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/lang_detect.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/library.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/lint.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/merge_view.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/models.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/persona.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/privacy.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/privacy_scan.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/prompt_dna.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/recommend.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/scorer.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/segmenter.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/session_meta.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/session_type.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/style.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/templates.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/timeutil.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/trends.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/core/wrapped.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/demo.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/embeddings/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/embeddings/base.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/embeddings/local_embed.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/embeddings/ollama.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/embeddings/openai_embed.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/embeddings/tfidf.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/mcp.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/mcp_main.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/agent_terminal.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/chartjs.min.js +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/compress_terminal.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/dashboard_terminal.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/distill_terminal.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/export.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/html_report.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/json_out.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/markdown.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/terminal.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/wrapped_html.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/output/wrapped_terminal.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/py.typed +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/sharing/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/sharing/client.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/sharing/clipboard.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/storage/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/telemetry/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/telemetry/collector.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/telemetry/consent.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/telemetry/events.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/telemetry/prompt.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/telemetry/queue.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/src/reprompt/telemetry/sender.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/__init__.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/conftest.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/fixtures/aider_chat_history.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/fixtures/chatgpt_conversations.json +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/fixtures/claude_chat_export.json +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/fixtures/claude_session.jsonl +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/fixtures/cline_task/api_conversation_history.json +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/fixtures/export/default_export.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/fixtures/export/full_export.md +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/fixtures/gemini_session.json +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/fixtures/openclaw_session.jsonl +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_adapter_aider.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_adapter_chatgpt.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_adapter_claude.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_adapter_claude_chat.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_adapter_cline.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_adapter_gemini.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_adapter_openclaw.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_agent.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_agent_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_analyzer.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_bridge_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_bridge_e2e.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_bridge_handler.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_bridge_integration.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_bridge_manifest.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_bridge_protocol.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_cli_deprecations.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_cli_library_effectiveness.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_clipboard.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_codex_adapter.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_compare_best_worst.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_compress.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_compress_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_compress_dna.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_compress_html.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_compress_insights.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_config.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_conversation.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_copy_flag.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_cost.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_coverage_boost.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_cursor_adapter.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_dashboard.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_db.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_db_digest.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_db_effectiveness.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_db_trends.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_dedup.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_demo.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_deprecated_commands.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_digest.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_digest_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_distill.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_distill_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_distill_weights.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_e2e.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_effectiveness.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_embeddings_local.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_embeddings_ollama.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_embeddings_openai.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_empty_state.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_export.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_export_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_export_snapshot.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_extractors.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_extractors_routing.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_extractors_zh.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_extractors_zh_e2e.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_html_report.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_import_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_import_e2e.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_insights.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_insights_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_insights_expanded.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_install_hook.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_lang_detect.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_library.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_lint.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_lint_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_markdown.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_mcp.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_merge_view.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_models.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_output.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_parse_conversation_base.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_parse_conversation_chatgpt.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_parse_conversation_claude.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_persona.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_pipeline.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_privacy.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_privacy_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_privacy_e2e.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_privacy_output.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_privacy_scan.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_prompt_dna.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_public_api.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_recommend.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_schema_version.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_score_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_scorer.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_segmenter.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_session_type.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_share_e2e.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_sharing_client.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_source_filter.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_style.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_style_trends.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_telemetry_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_telemetry_collector.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_telemetry_consent.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_telemetry_e2e.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_telemetry_events.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_telemetry_prompt.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_telemetry_queue.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_telemetry_sender.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_template_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_templates.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_timeutil.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_trends.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_trends_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_use_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_wrapped.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_wrapped_cli.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_wrapped_e2e.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_wrapped_html.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_wrapped_output.py +0 -0
- {reprompt_cli-1.7.1 → reprompt_cli-1.8.1}/tests/test_wrapped_share.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reprompt-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.8.1
|
|
4
4
|
Summary: Discover, analyze, and optimize your prompts from AI coding sessions
|
|
5
5
|
Project-URL: Homepage, https://github.com/reprompt-dev/reprompt
|
|
6
6
|
Project-URL: Repository, https://github.com/reprompt-dev/reprompt
|
|
@@ -185,7 +185,7 @@ reprompt install-hook # adds post-session hook to Claude Code
|
|
|
185
185
|
|
|
186
186
|
Capture prompts from ChatGPT, Claude.ai, and Gemini directly in your browser:
|
|
187
187
|
|
|
188
|
-
1. **Install the extension** from [Chrome Web Store](https://chromewebstore.google.com/detail/reprompt/ojdccpagaanchmkninlbgbgemdcjckhn)
|
|
188
|
+
1. **Install the extension** from [Chrome Web Store](https://chromewebstore.google.com/detail/reprompt/ojdccpagaanchmkninlbgbgemdcjckhn) or [Firefox Add-ons](https://addons.mozilla.org/addon/reprompt-cli/)
|
|
189
189
|
2. **Connect to the CLI:** `reprompt install-extension`
|
|
190
190
|
3. **Verify:** `reprompt extension-status`
|
|
191
191
|
|
|
@@ -236,6 +236,7 @@ reprompt lint --json # machine-readable output
|
|
|
236
236
|
|
|
237
237
|
- **Website:** [getreprompt.dev](https://getreprompt.dev)
|
|
238
238
|
- **Chrome Extension:** [Chrome Web Store](https://chromewebstore.google.com/detail/reprompt/ojdccpagaanchmkninlbgbgemdcjckhn)
|
|
239
|
+
- **Firefox Add-on:** [Firefox Add-ons](https://addons.mozilla.org/addon/reprompt-cli/)
|
|
239
240
|
- **PyPI:** [reprompt-cli](https://pypi.org/project/reprompt-cli/)
|
|
240
241
|
- **Changelog:** [CHANGELOG.md](CHANGELOG.md)
|
|
241
242
|
- **Privacy:** [getreprompt.dev/privacy](https://getreprompt.dev/privacy)
|
|
@@ -140,7 +140,7 @@ reprompt install-hook # adds post-session hook to Claude Code
|
|
|
140
140
|
|
|
141
141
|
Capture prompts from ChatGPT, Claude.ai, and Gemini directly in your browser:
|
|
142
142
|
|
|
143
|
-
1. **Install the extension** from [Chrome Web Store](https://chromewebstore.google.com/detail/reprompt/ojdccpagaanchmkninlbgbgemdcjckhn)
|
|
143
|
+
1. **Install the extension** from [Chrome Web Store](https://chromewebstore.google.com/detail/reprompt/ojdccpagaanchmkninlbgbgemdcjckhn) or [Firefox Add-ons](https://addons.mozilla.org/addon/reprompt-cli/)
|
|
144
144
|
2. **Connect to the CLI:** `reprompt install-extension`
|
|
145
145
|
3. **Verify:** `reprompt extension-status`
|
|
146
146
|
|
|
@@ -191,6 +191,7 @@ reprompt lint --json # machine-readable output
|
|
|
191
191
|
|
|
192
192
|
- **Website:** [getreprompt.dev](https://getreprompt.dev)
|
|
193
193
|
- **Chrome Extension:** [Chrome Web Store](https://chromewebstore.google.com/detail/reprompt/ojdccpagaanchmkninlbgbgemdcjckhn)
|
|
194
|
+
- **Firefox Add-on:** [Firefox Add-ons](https://addons.mozilla.org/addon/reprompt-cli/)
|
|
194
195
|
- **PyPI:** [reprompt-cli](https://pypi.org/project/reprompt-cli/)
|
|
195
196
|
- **Changelog:** [CHANGELOG.md](CHANGELOG.md)
|
|
196
197
|
- **Privacy:** [getreprompt.dev/privacy](https://getreprompt.dev/privacy)
|
|
@@ -1474,6 +1474,7 @@ def insights(
|
|
|
1474
1474
|
from reprompt.config import Settings
|
|
1475
1475
|
from reprompt.core.insights import (
|
|
1476
1476
|
compute_insights,
|
|
1477
|
+
get_cross_session_repetition_insight,
|
|
1477
1478
|
get_effectiveness_insight,
|
|
1478
1479
|
get_similar_prompts_insight,
|
|
1479
1480
|
)
|
|
@@ -1487,12 +1488,14 @@ def insights(
|
|
|
1487
1488
|
# Expanded sections
|
|
1488
1489
|
eff_data = get_effectiveness_insight(db, source=source)
|
|
1489
1490
|
sim_data = get_similar_prompts_insight(db, source=source)
|
|
1491
|
+
rep_data = get_cross_session_repetition_insight(db, source=source)
|
|
1490
1492
|
|
|
1491
1493
|
if json_output:
|
|
1492
1494
|
import json as json_mod
|
|
1493
1495
|
|
|
1494
1496
|
result["effectiveness"] = eff_data
|
|
1495
1497
|
result["similar_prompts"] = sim_data
|
|
1498
|
+
result["cross_session_repetition"] = rep_data
|
|
1496
1499
|
typer.echo(json_mod.dumps(result, indent=2))
|
|
1497
1500
|
else:
|
|
1498
1501
|
from reprompt.core.suggestions import get_suggestion
|
|
@@ -1516,6 +1519,15 @@ def insights(
|
|
|
1516
1519
|
' [dim]\u2192 Try: reprompt template save "..." (reuse instead of rewrite)[/dim]'
|
|
1517
1520
|
)
|
|
1518
1521
|
|
|
1522
|
+
if rep_data:
|
|
1523
|
+
rate_pct = f"{rep_data['repetition_rate'] * 100:.0f}%"
|
|
1524
|
+
n = rep_data["total_recurring_topics"]
|
|
1525
|
+
console.print("\n [bold]Cross-Session Repetition[/bold]")
|
|
1526
|
+
console.print(f" {rate_pct} of prompts recur across sessions ({n} topics)")
|
|
1527
|
+
for t in rep_data["top_topics"]:
|
|
1528
|
+
console.print(f' "{t["canonical_text"]}" \u2014 {t["session_count"]} sessions')
|
|
1529
|
+
console.print(" [dim]\u2192 Try: reprompt repetition (full analysis)[/dim]")
|
|
1530
|
+
|
|
1519
1531
|
hint = get_suggestion("insights")
|
|
1520
1532
|
if hint:
|
|
1521
1533
|
console.print(f"\n [dim]\u2192 Try: {hint}[/dim]")
|
|
@@ -1525,6 +1537,7 @@ def insights(
|
|
|
1525
1537
|
|
|
1526
1538
|
result["effectiveness"] = eff_data
|
|
1527
1539
|
result["similar_prompts"] = sim_data
|
|
1540
|
+
result["cross_session_repetition"] = rep_data
|
|
1528
1541
|
_copy_to_clip(json_mod.dumps(result, indent=2), quiet=json_output)
|
|
1529
1542
|
|
|
1530
1543
|
|
|
@@ -1856,7 +1869,7 @@ def _create_host_wrapper() -> Path:
|
|
|
1856
1869
|
# Find the Python executable that has reprompt installed
|
|
1857
1870
|
python_path = sys_mod.executable
|
|
1858
1871
|
|
|
1859
|
-
wrapper_path.write_text(f
|
|
1872
|
+
wrapper_path.write_text(f'#!/bin/sh\nexec "{python_path}" -u -m reprompt.bridge.host\n')
|
|
1860
1873
|
wrapper_path.chmod(wrapper_path.stat().st_mode | stat.S_IEXEC)
|
|
1861
1874
|
return wrapper_path
|
|
1862
1875
|
|
|
@@ -1916,6 +1929,102 @@ def extension_status() -> None:
|
|
|
1916
1929
|
console.print(" Last sync: never")
|
|
1917
1930
|
|
|
1918
1931
|
|
|
1932
|
+
@app.command(rich_help_panel="Analyze")
|
|
1933
|
+
def sessions(
|
|
1934
|
+
last: int = typer.Option(10, "--last", help="Show N most recent sessions"),
|
|
1935
|
+
source: str = typer.Option(
|
|
1936
|
+
None, "--source", "-s", help="Filter by source (e.g. claude-code, cursor)"
|
|
1937
|
+
),
|
|
1938
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
1939
|
+
detail: str = typer.Option(None, "--detail", help="Deep-dive into a session ID"),
|
|
1940
|
+
copy: bool = typer.Option(False, "--copy", help="Copy result to clipboard"),
|
|
1941
|
+
) -> None:
|
|
1942
|
+
"""Session quality overview: composite scores, frustration signals, trends."""
|
|
1943
|
+
import json as json_mod
|
|
1944
|
+
|
|
1945
|
+
from reprompt.config import Settings
|
|
1946
|
+
from reprompt.output.sessions_terminal import render_session_detail, render_sessions_table
|
|
1947
|
+
from reprompt.storage.db import PromptDB
|
|
1948
|
+
|
|
1949
|
+
settings = Settings()
|
|
1950
|
+
db = PromptDB(settings.db_path)
|
|
1951
|
+
|
|
1952
|
+
if detail:
|
|
1953
|
+
# Single session detail view
|
|
1954
|
+
all_sessions = db.get_sessions_with_quality(limit=500)
|
|
1955
|
+
match = next((s for s in all_sessions if s.get("session_id") == detail), None)
|
|
1956
|
+
if not match:
|
|
1957
|
+
# Try prefix match
|
|
1958
|
+
match = next(
|
|
1959
|
+
(s for s in all_sessions if (s.get("session_id") or "").startswith(detail)),
|
|
1960
|
+
None,
|
|
1961
|
+
)
|
|
1962
|
+
if not match:
|
|
1963
|
+
typer.echo(f"Session '{detail}' not found.")
|
|
1964
|
+
raise typer.Exit(1)
|
|
1965
|
+
if json_output:
|
|
1966
|
+
typer.echo(json_mod.dumps(match, indent=2, default=str))
|
|
1967
|
+
else:
|
|
1968
|
+
typer.echo(render_session_detail(match), nl=False)
|
|
1969
|
+
else:
|
|
1970
|
+
data = db.get_sessions_with_quality(limit=last, source=source)
|
|
1971
|
+
if json_output:
|
|
1972
|
+
typer.echo(json_mod.dumps(data, indent=2, default=str))
|
|
1973
|
+
else:
|
|
1974
|
+
typer.echo(render_sessions_table(data), nl=False)
|
|
1975
|
+
|
|
1976
|
+
from reprompt.core.suggestions import get_suggestion
|
|
1977
|
+
|
|
1978
|
+
hint = get_suggestion("sessions")
|
|
1979
|
+
if hint:
|
|
1980
|
+
console.print(f"\n [dim]\u2192 Try: {hint}[/dim]")
|
|
1981
|
+
|
|
1982
|
+
if copy:
|
|
1983
|
+
if detail:
|
|
1984
|
+
copy_text = json_mod.dumps(match, indent=2, default=str) # type: ignore[possibly-undefined]
|
|
1985
|
+
else:
|
|
1986
|
+
copy_text = json_mod.dumps(data, indent=2, default=str) # type: ignore[possibly-undefined]
|
|
1987
|
+
_copy_to_clip(copy_text, quiet=json_output)
|
|
1988
|
+
|
|
1989
|
+
|
|
1990
|
+
@app.command(rich_help_panel="Analyze")
|
|
1991
|
+
def repetition(
|
|
1992
|
+
last: int = typer.Option(500, "--last", help="Analyze N most recent unique prompts"),
|
|
1993
|
+
source: str = typer.Option(
|
|
1994
|
+
None, "--source", "-s", help="Filter by source (e.g. claude-code, cursor)"
|
|
1995
|
+
),
|
|
1996
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
1997
|
+
copy: bool = typer.Option(False, "--copy", help="Copy result to clipboard"),
|
|
1998
|
+
) -> None:
|
|
1999
|
+
"""Detect recurring prompts across different sessions."""
|
|
2000
|
+
import json as json_mod
|
|
2001
|
+
from dataclasses import asdict
|
|
2002
|
+
|
|
2003
|
+
from reprompt.config import Settings
|
|
2004
|
+
from reprompt.core.repetition import analyze_repetition
|
|
2005
|
+
from reprompt.output.repetition_terminal import render_repetition_report
|
|
2006
|
+
from reprompt.storage.db import PromptDB
|
|
2007
|
+
|
|
2008
|
+
settings = Settings()
|
|
2009
|
+
db = PromptDB(settings.db_path)
|
|
2010
|
+
report = analyze_repetition(db, source=source, limit=last)
|
|
2011
|
+
|
|
2012
|
+
if json_output:
|
|
2013
|
+
typer.echo(json_mod.dumps(asdict(report), indent=2, default=str))
|
|
2014
|
+
else:
|
|
2015
|
+
typer.echo(render_repetition_report(report), nl=False)
|
|
2016
|
+
|
|
2017
|
+
from reprompt.core.suggestions import get_suggestion
|
|
2018
|
+
|
|
2019
|
+
hint = get_suggestion("repetition")
|
|
2020
|
+
if hint:
|
|
2021
|
+
console.print(f"\n [dim]\u2192 Try: {hint}[/dim]")
|
|
2022
|
+
|
|
2023
|
+
if copy:
|
|
2024
|
+
copy_text = json_mod.dumps(asdict(report), indent=2, default=str)
|
|
2025
|
+
_copy_to_clip(copy_text, quiet=json_output)
|
|
2026
|
+
|
|
2027
|
+
|
|
1919
2028
|
@app.command(rich_help_panel="Analyze")
|
|
1920
2029
|
def agent(
|
|
1921
2030
|
last: int = typer.Option(5, "--last", help="Analyze N most recent sessions"),
|
|
@@ -293,3 +293,29 @@ def get_similar_prompts_insight(
|
|
|
293
293
|
"total_clusters": len(clusters),
|
|
294
294
|
"total_clustered_prompts": sum(c["size"] for c in clusters),
|
|
295
295
|
}
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def get_cross_session_repetition_insight(
|
|
299
|
+
db: PromptDB,
|
|
300
|
+
source: str | None = None,
|
|
301
|
+
) -> dict[str, Any] | None:
|
|
302
|
+
"""Return cross-session repetition summary, or None if no recurring topics."""
|
|
303
|
+
from reprompt.core.repetition import analyze_repetition
|
|
304
|
+
|
|
305
|
+
report = analyze_repetition(db, source=source, limit=500)
|
|
306
|
+
|
|
307
|
+
if not report.recurring_topics:
|
|
308
|
+
return None
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
"repetition_rate": report.repetition_rate,
|
|
312
|
+
"top_topics": [
|
|
313
|
+
{
|
|
314
|
+
"canonical_text": t.canonical_text[:80],
|
|
315
|
+
"session_count": t.session_count,
|
|
316
|
+
"total_matches": t.total_matches,
|
|
317
|
+
}
|
|
318
|
+
for t in report.recurring_topics[:3]
|
|
319
|
+
],
|
|
320
|
+
"total_recurring_topics": len(report.recurring_topics),
|
|
321
|
+
}
|
|
@@ -185,6 +185,105 @@ def run_scan(
|
|
|
185
185
|
except Exception:
|
|
186
186
|
logger.debug("Session metadata extraction failed for %s", file_path, exc_info=True)
|
|
187
187
|
|
|
188
|
+
# Compute session quality scores
|
|
189
|
+
from datetime import datetime
|
|
190
|
+
|
|
191
|
+
from reprompt.core.agent import analyze_session
|
|
192
|
+
from reprompt.core.conversation import Conversation
|
|
193
|
+
from reprompt.core.distill import distill_conversation
|
|
194
|
+
from reprompt.core.session_quality import score_session
|
|
195
|
+
|
|
196
|
+
quality_failures = 0
|
|
197
|
+
for file_path, adapter_name in scanned_files:
|
|
198
|
+
try:
|
|
199
|
+
matched = next((a for a in adapters if a.name == adapter_name), None)
|
|
200
|
+
if not matched:
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
# parse_conversation returns list[ConversationTurn], wrap into Conversation
|
|
204
|
+
turns = matched.parse_conversation(Path(file_path))
|
|
205
|
+
if not turns:
|
|
206
|
+
continue
|
|
207
|
+
|
|
208
|
+
session_id = Path(file_path).stem
|
|
209
|
+
start_time = None
|
|
210
|
+
end_time = None
|
|
211
|
+
duration = None
|
|
212
|
+
timestamps = [t.timestamp for t in turns if t.timestamp]
|
|
213
|
+
if len(timestamps) >= 2:
|
|
214
|
+
start_time = timestamps[0]
|
|
215
|
+
end_time = timestamps[-1]
|
|
216
|
+
try:
|
|
217
|
+
start_dt = datetime.fromisoformat(start_time.replace("Z", "+00:00"))
|
|
218
|
+
end_dt = datetime.fromisoformat(end_time.replace("Z", "+00:00"))
|
|
219
|
+
duration = int((end_dt - start_dt).total_seconds())
|
|
220
|
+
except (ValueError, TypeError):
|
|
221
|
+
pass
|
|
222
|
+
|
|
223
|
+
project = None
|
|
224
|
+
if hasattr(matched, "_project_from_path"):
|
|
225
|
+
project = matched._project_from_path(file_path)
|
|
226
|
+
|
|
227
|
+
conversation = Conversation(
|
|
228
|
+
session_id=session_id,
|
|
229
|
+
source=adapter_name,
|
|
230
|
+
project=project,
|
|
231
|
+
turns=turns,
|
|
232
|
+
start_time=start_time,
|
|
233
|
+
end_time=end_time,
|
|
234
|
+
duration_seconds=duration,
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
# Agent analysis (efficiency, error loops)
|
|
238
|
+
agent_report = None
|
|
239
|
+
try:
|
|
240
|
+
agent_report = analyze_session(conversation)
|
|
241
|
+
except Exception:
|
|
242
|
+
logger.warning("Agent analysis failed for %s", file_path, exc_info=True)
|
|
243
|
+
|
|
244
|
+
# Distill analysis (focus/retention)
|
|
245
|
+
distill_result = None
|
|
246
|
+
try:
|
|
247
|
+
distill_result = distill_conversation(conversation)
|
|
248
|
+
except Exception:
|
|
249
|
+
logger.warning("Distill analysis failed for %s", file_path, exc_info=True)
|
|
250
|
+
|
|
251
|
+
# Avg prompt score from stored features
|
|
252
|
+
avg_prompt_score = None
|
|
253
|
+
scores = db.get_prompt_scores_for_session(session_id)
|
|
254
|
+
if scores:
|
|
255
|
+
avg_prompt_score = sum(scores) / len(scores)
|
|
256
|
+
|
|
257
|
+
# Effectiveness score from session_meta
|
|
258
|
+
effectiveness = db.get_effectiveness_for_session(session_id)
|
|
259
|
+
|
|
260
|
+
quality = score_session(
|
|
261
|
+
conversation,
|
|
262
|
+
agent_report=agent_report,
|
|
263
|
+
distill_result=distill_result,
|
|
264
|
+
effectiveness_score=effectiveness,
|
|
265
|
+
avg_prompt_score=avg_prompt_score,
|
|
266
|
+
)
|
|
267
|
+
db.upsert_session_quality(
|
|
268
|
+
session_id=quality.session_id,
|
|
269
|
+
quality_score=quality.quality_score,
|
|
270
|
+
prompt_quality_score=quality.prompt_quality,
|
|
271
|
+
efficiency_score=quality.efficiency,
|
|
272
|
+
focus_score=quality.focus,
|
|
273
|
+
outcome_score=quality.outcome,
|
|
274
|
+
has_abandonment=quality.frustration.abandonment,
|
|
275
|
+
has_escalation=quality.frustration.escalation,
|
|
276
|
+
stall_turns=quality.frustration.stall_turns,
|
|
277
|
+
session_type=quality.session_type,
|
|
278
|
+
quality_insight=quality.insight,
|
|
279
|
+
)
|
|
280
|
+
except Exception:
|
|
281
|
+
quality_failures += 1
|
|
282
|
+
logger.warning("Session quality scoring failed for %s", file_path, exc_info=True)
|
|
283
|
+
|
|
284
|
+
if quality_failures:
|
|
285
|
+
logger.warning("Quality scoring failed for %d session(s)", quality_failures)
|
|
286
|
+
|
|
188
287
|
# Mark sessions processed only after successful dedup+store
|
|
189
288
|
for file_path, adapter_name in scanned_files:
|
|
190
289
|
db.mark_session_processed(file_path, source=adapter_name)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""Cross-session prompt repetition detection.
|
|
2
|
+
|
|
3
|
+
Identifies recurring topics asked across different AI coding sessions
|
|
4
|
+
using TF-IDF + containment similarity clustering. All analysis is
|
|
5
|
+
rule-based (zero LLM).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from dataclasses import dataclass, field
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from reprompt.storage.db import PromptDB
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class RecurringTopic:
|
|
18
|
+
"""A topic that recurs across multiple sessions."""
|
|
19
|
+
|
|
20
|
+
canonical_text: str
|
|
21
|
+
session_count: int
|
|
22
|
+
total_matches: int
|
|
23
|
+
session_ids: list[str] = field(default_factory=list)
|
|
24
|
+
earliest: str = ""
|
|
25
|
+
latest: str = ""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class RepetitionReport:
|
|
30
|
+
"""Result of cross-session repetition analysis."""
|
|
31
|
+
|
|
32
|
+
total_prompts_analyzed: int = 0
|
|
33
|
+
cross_session_matches: int = 0
|
|
34
|
+
repetition_rate: float = 0.0 # cross_session_matches / total
|
|
35
|
+
recurring_topics: list[RecurringTopic] = field(default_factory=list)
|
|
36
|
+
total_sessions: int = 0
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def analyze_repetition(
|
|
40
|
+
db: PromptDB,
|
|
41
|
+
source: str | None = None,
|
|
42
|
+
limit: int = 500,
|
|
43
|
+
threshold: float = 0.75,
|
|
44
|
+
) -> RepetitionReport:
|
|
45
|
+
"""Detect recurring prompts across different sessions.
|
|
46
|
+
|
|
47
|
+
Reuses merge_view.build_clusters() for similarity, then filters
|
|
48
|
+
to clusters spanning 2+ distinct sessions.
|
|
49
|
+
"""
|
|
50
|
+
from reprompt.core.merge_view import build_clusters
|
|
51
|
+
|
|
52
|
+
all_prompts = db.get_all_prompts(source=source)
|
|
53
|
+
|
|
54
|
+
# Filter to unique prompts only
|
|
55
|
+
unique = [p for p in all_prompts if p.get("duplicate_of") is None]
|
|
56
|
+
|
|
57
|
+
if not unique:
|
|
58
|
+
return RepetitionReport()
|
|
59
|
+
|
|
60
|
+
# Limit to most recent N (by id desc), then reverse for chronological
|
|
61
|
+
unique.sort(key=lambda p: p.get("id", 0), reverse=True)
|
|
62
|
+
unique = unique[:limit]
|
|
63
|
+
unique.reverse()
|
|
64
|
+
|
|
65
|
+
# Build lookup: text → prompt dict (safe due to hash uniqueness)
|
|
66
|
+
text_to_prompt: dict[str, dict[str, Any]] = {}
|
|
67
|
+
for p in unique:
|
|
68
|
+
text_to_prompt[p["text"]] = p
|
|
69
|
+
|
|
70
|
+
texts = [p["text"] for p in unique]
|
|
71
|
+
timestamps = [p.get("timestamp", "") for p in unique]
|
|
72
|
+
all_session_ids = {p.get("session_id") or "unknown" for p in unique}
|
|
73
|
+
|
|
74
|
+
# Build clusters using existing infrastructure
|
|
75
|
+
clusters = build_clusters(texts, timestamps, threshold=threshold)
|
|
76
|
+
|
|
77
|
+
# Filter to cross-session clusters
|
|
78
|
+
recurring: list[RecurringTopic] = []
|
|
79
|
+
total_cross_matches = 0
|
|
80
|
+
|
|
81
|
+
for cluster in clusters:
|
|
82
|
+
# Collect all texts in cluster (canonical + members)
|
|
83
|
+
cluster_texts = [cluster["canonical"]["text"]]
|
|
84
|
+
cluster_texts.extend(m["text"] for m in cluster["members"])
|
|
85
|
+
|
|
86
|
+
# Map to session_ids
|
|
87
|
+
sids: list[str] = []
|
|
88
|
+
cluster_timestamps: list[str] = []
|
|
89
|
+
for t in cluster_texts:
|
|
90
|
+
prompt = text_to_prompt.get(t)
|
|
91
|
+
if prompt:
|
|
92
|
+
sids.append(prompt.get("session_id") or "unknown")
|
|
93
|
+
cluster_timestamps.append(prompt.get("timestamp", ""))
|
|
94
|
+
|
|
95
|
+
distinct_sessions = sorted(set(sids))
|
|
96
|
+
if len(distinct_sessions) < 2:
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
# Build timestamps for range
|
|
100
|
+
valid_ts = sorted(t for t in cluster_timestamps if t)
|
|
101
|
+
earliest = valid_ts[0] if valid_ts else ""
|
|
102
|
+
latest = valid_ts[-1] if valid_ts else ""
|
|
103
|
+
|
|
104
|
+
recurring.append(
|
|
105
|
+
RecurringTopic(
|
|
106
|
+
canonical_text=cluster["canonical"]["text"],
|
|
107
|
+
session_count=len(distinct_sessions),
|
|
108
|
+
total_matches=len(cluster_texts),
|
|
109
|
+
session_ids=distinct_sessions,
|
|
110
|
+
earliest=earliest,
|
|
111
|
+
latest=latest,
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
total_cross_matches += len(cluster_texts)
|
|
115
|
+
|
|
116
|
+
# Sort by session_count desc, then total_matches desc
|
|
117
|
+
recurring.sort(key=lambda t: (-t.session_count, -t.total_matches))
|
|
118
|
+
|
|
119
|
+
total = len(unique)
|
|
120
|
+
rate = total_cross_matches / total if total > 0 else 0.0
|
|
121
|
+
|
|
122
|
+
return RepetitionReport(
|
|
123
|
+
total_prompts_analyzed=total,
|
|
124
|
+
cross_session_matches=total_cross_matches,
|
|
125
|
+
repetition_rate=round(rate, 3),
|
|
126
|
+
recurring_topics=recurring,
|
|
127
|
+
total_sessions=len(all_session_ids),
|
|
128
|
+
)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""Session-level quality metrics — composite scoring + frustration detection.
|
|
2
|
+
|
|
3
|
+
Combines prompt quality, agent efficiency, distill focus, and outcome
|
|
4
|
+
into a single 0-100 session score. All analysis is rule-based (zero LLM, <1ms).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass, field
|
|
10
|
+
|
|
11
|
+
from reprompt.core.agent import AgentReport
|
|
12
|
+
from reprompt.core.conversation import Conversation, ConversationTurn, DistillResult
|
|
13
|
+
|
|
14
|
+
# ---------------------------------------------------------------------------
|
|
15
|
+
# Component weights (must sum to 1.0)
|
|
16
|
+
# ---------------------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
DEFAULT_WEIGHTS: dict[str, float] = {
|
|
19
|
+
"prompt_quality": 0.30,
|
|
20
|
+
"efficiency": 0.30,
|
|
21
|
+
"focus": 0.20,
|
|
22
|
+
"outcome": 0.20,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# ---------------------------------------------------------------------------
|
|
27
|
+
# Dataclasses
|
|
28
|
+
# ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class FrustrationSignals:
|
|
33
|
+
"""Frustration indicators detected from conversation turns."""
|
|
34
|
+
|
|
35
|
+
abandonment: bool = False # Last 3+ assistant turns all have errors
|
|
36
|
+
escalation: bool = False # Error rate in second half > first half * 1.5
|
|
37
|
+
stall_turns: int = 0 # Assistant turns with 0 tool calls and <50 chars
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass
|
|
41
|
+
class SessionQuality:
|
|
42
|
+
"""Composite session quality report."""
|
|
43
|
+
|
|
44
|
+
session_id: str
|
|
45
|
+
quality_score: float # 0-100 composite
|
|
46
|
+
prompt_quality: float | None = None # 0-100
|
|
47
|
+
efficiency: float | None = None # 0-100
|
|
48
|
+
focus: float | None = None # 0-100
|
|
49
|
+
outcome: float | None = None # 0-100
|
|
50
|
+
frustration: FrustrationSignals = field(default_factory=FrustrationSignals)
|
|
51
|
+
session_type: str | None = None
|
|
52
|
+
insight: str = ""
|
|
53
|
+
components_available: int = 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Frustration detection
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _detect_frustration(turns: list[ConversationTurn]) -> FrustrationSignals:
|
|
62
|
+
"""Detect frustration signals from conversation turns."""
|
|
63
|
+
asst_turns = [t for t in turns if t.role == "assistant"]
|
|
64
|
+
|
|
65
|
+
if not asst_turns:
|
|
66
|
+
return FrustrationSignals()
|
|
67
|
+
|
|
68
|
+
# Abandonment: last 3+ assistant turns all have errors
|
|
69
|
+
abandonment = False
|
|
70
|
+
if len(asst_turns) >= 3:
|
|
71
|
+
last_three = asst_turns[-3:]
|
|
72
|
+
abandonment = all(t.has_error for t in last_three)
|
|
73
|
+
|
|
74
|
+
# Escalation: error rate in second half > first half * 1.5
|
|
75
|
+
escalation = False
|
|
76
|
+
if len(asst_turns) >= 4:
|
|
77
|
+
mid = len(asst_turns) // 2
|
|
78
|
+
first_half = asst_turns[:mid]
|
|
79
|
+
second_half = asst_turns[mid:]
|
|
80
|
+
first_rate = sum(1 for t in first_half if t.has_error) / len(first_half)
|
|
81
|
+
second_rate = sum(1 for t in second_half if t.has_error) / len(second_half)
|
|
82
|
+
escalation = second_rate > first_rate * 1.5 and second_rate > 0.2
|
|
83
|
+
|
|
84
|
+
# Stall turns: assistant turns with no tool calls and short text
|
|
85
|
+
stall_turns = sum(1 for t in asst_turns if t.tool_calls == 0 and len(t.text.strip()) < 50)
|
|
86
|
+
|
|
87
|
+
return FrustrationSignals(
|
|
88
|
+
abandonment=abandonment,
|
|
89
|
+
escalation=escalation,
|
|
90
|
+
stall_turns=stall_turns,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Insight generation
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _generate_insight(quality: SessionQuality) -> str:
|
|
100
|
+
"""Generate a one-line insight from quality metrics (priority order)."""
|
|
101
|
+
f = quality.frustration
|
|
102
|
+
|
|
103
|
+
if f.abandonment:
|
|
104
|
+
return "Ended with unresolved errors"
|
|
105
|
+
|
|
106
|
+
if f.escalation:
|
|
107
|
+
return "Errors escalated through session"
|
|
108
|
+
|
|
109
|
+
if f.stall_turns >= 5:
|
|
110
|
+
return f"{f.stall_turns} stall turns detected"
|
|
111
|
+
|
|
112
|
+
if quality.efficiency is not None and quality.efficiency < 50:
|
|
113
|
+
return "Low efficiency (error loops)"
|
|
114
|
+
|
|
115
|
+
score = quality.quality_score
|
|
116
|
+
if score >= 80:
|
|
117
|
+
return "Focused session"
|
|
118
|
+
if score >= 60:
|
|
119
|
+
return "Solid session"
|
|
120
|
+
if score >= 40:
|
|
121
|
+
return "Room for improvement"
|
|
122
|
+
return "Rough session"
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ---------------------------------------------------------------------------
|
|
126
|
+
# Main scoring function
|
|
127
|
+
# ---------------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def score_session(
|
|
131
|
+
conversation: Conversation,
|
|
132
|
+
*,
|
|
133
|
+
agent_report: AgentReport | None = None,
|
|
134
|
+
distill_result: DistillResult | None = None,
|
|
135
|
+
effectiveness_score: float | None = None,
|
|
136
|
+
avg_prompt_score: float | None = None,
|
|
137
|
+
) -> SessionQuality:
|
|
138
|
+
"""Compute composite session quality score (0-100).
|
|
139
|
+
|
|
140
|
+
Components (weighted average of available inputs):
|
|
141
|
+
- prompt_quality (30%): avg overall_score from prompt features
|
|
142
|
+
- efficiency (30%): productive_ratio from agent analysis
|
|
143
|
+
- focus (20%): retention_ratio from distill
|
|
144
|
+
- outcome (20%): effectiveness_score
|
|
145
|
+
|
|
146
|
+
When a component is unavailable, its weight redistributes proportionally.
|
|
147
|
+
"""
|
|
148
|
+
components: dict[str, float] = {}
|
|
149
|
+
|
|
150
|
+
# Prompt quality: already 0-100
|
|
151
|
+
if avg_prompt_score is not None:
|
|
152
|
+
components["prompt_quality"] = max(0.0, min(100.0, avg_prompt_score))
|
|
153
|
+
|
|
154
|
+
# Efficiency: productive_ratio is 0-1
|
|
155
|
+
if agent_report is not None:
|
|
156
|
+
ratio = agent_report.efficiency.productive_ratio
|
|
157
|
+
components["efficiency"] = max(0.0, min(100.0, ratio * 100))
|
|
158
|
+
|
|
159
|
+
# Focus: retention_ratio is 0-1
|
|
160
|
+
if distill_result is not None:
|
|
161
|
+
ratio = distill_result.stats.retention_ratio
|
|
162
|
+
components["focus"] = max(0.0, min(100.0, ratio * 100))
|
|
163
|
+
|
|
164
|
+
# Outcome: effectiveness_score is 0-1
|
|
165
|
+
if effectiveness_score is not None:
|
|
166
|
+
components["outcome"] = max(0.0, min(100.0, effectiveness_score * 100))
|
|
167
|
+
|
|
168
|
+
# Compute weighted average with weight redistribution
|
|
169
|
+
if components:
|
|
170
|
+
available_weights = {k: DEFAULT_WEIGHTS[k] for k in components}
|
|
171
|
+
weight_sum = sum(available_weights.values())
|
|
172
|
+
normalized = {k: w / weight_sum for k, w in available_weights.items()}
|
|
173
|
+
quality_score = sum(components[k] * normalized[k] for k in components)
|
|
174
|
+
else:
|
|
175
|
+
quality_score = 0.0
|
|
176
|
+
|
|
177
|
+
quality_score = round(max(0.0, min(100.0, quality_score)), 1)
|
|
178
|
+
|
|
179
|
+
# Frustration detection
|
|
180
|
+
frustration = _detect_frustration(conversation.turns)
|
|
181
|
+
|
|
182
|
+
# Session type
|
|
183
|
+
session_type_str: str | None = None
|
|
184
|
+
if agent_report is not None and agent_report.efficiency.session_type is not None:
|
|
185
|
+
session_type_str = agent_report.efficiency.session_type
|
|
186
|
+
|
|
187
|
+
quality = SessionQuality(
|
|
188
|
+
session_id=conversation.session_id,
|
|
189
|
+
quality_score=quality_score,
|
|
190
|
+
prompt_quality=components.get("prompt_quality"),
|
|
191
|
+
efficiency=components.get("efficiency"),
|
|
192
|
+
focus=components.get("focus"),
|
|
193
|
+
outcome=components.get("outcome"),
|
|
194
|
+
frustration=frustration,
|
|
195
|
+
session_type=session_type_str,
|
|
196
|
+
components_available=len(components),
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
quality.insight = _generate_insight(quality)
|
|
200
|
+
|
|
201
|
+
return quality
|