devmemory-cli 0.1.0.dev0__tar.gz → 0.1.0.dev1__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.
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/PKG-INFO +1 -1
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/__about__.py +1 -1
- devmemory_cli-0.1.0.dev1/src/devmemory/adapters/genie.py +161 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/api/app.py +27 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/api/schemas.py +15 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/config.py +5 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/App.tsx +2 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/api/client.ts +18 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/api/types.ts +20 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/components/Sidebar.tsx +1 -0
- devmemory_cli-0.1.0.dev1/src/devmemory/web/frontend/src/routes/Ask.tsx +235 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/theme/components.css +90 -0
- devmemory_cli-0.1.0.dev0/src/devmemory/web/static/assets/index-CbV5njRH.js → devmemory_cli-0.1.0.dev1/src/devmemory/web/static/assets/index-B9obAJEW.js +20 -15
- devmemory_cli-0.1.0.dev0/src/devmemory/web/static/assets/index-DD-7ceZx.css → devmemory_cli-0.1.0.dev1/src/devmemory/web/static/assets/index-C81gevra.css +1 -1
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/static/index.html +2 -2
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_api.py +40 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/.gitignore +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/CHANGELOG.md +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/DEMO.md +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/LICENSE +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/README.md +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/docs/ARCHITECTURE.md +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/docs/CONFIGURATION.md +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/docs/DATA_AND_API_SPEC.md +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/docs/ENTIRE_INTEGRATION.md +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/docs/ENTIRE_QUICKSTART.md +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/docs/IMPLEMENTATION_STRATEGY.md +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/docs/MCP.md +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/docs/PROJECT_SPEC.md +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/docs/STATE_LOOP.md +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/examples/demo/seed.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/pyproject.toml +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/__init__.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/__main__.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/adapters/__init__.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/adapters/databricks.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/adapters/entire.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/adapters/git.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/adapters/graph.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/adapters/metrics.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/adapters/tests.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/analysis/__init__.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/analysis/base.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/analysis/chain.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/analysis/llm.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/analysis/rules.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/api/__init__.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/api/mappers.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/__init__.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/_errors.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/_render.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/analytics.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/analyze.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/app.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/checkpoint.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/compare.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/doctor.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/history.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/impact.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/init.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/mcp.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/memory.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/restore.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/search.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/serve.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/show.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/status.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/cli/task.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/domain/__init__.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/domain/enums.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/domain/errors.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/domain/models.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/domain/taskloop.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/environment.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/logging.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/mcp/__init__.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/mcp/server.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/paths.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/pipeline/__init__.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/pipeline/checkpoint.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/pipeline/feature_detect.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/pipeline/regression.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/pipeline/runlog.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/pipeline/status_rules.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/py.typed +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/services/__init__.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/services/agent_context.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/services/analysis.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/services/analytics.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/services/brief.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/services/context.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/services/databricks_sync.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/services/features.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/services/impact.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/services/memory.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/services/projects.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/services/restore.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/services/taskloop/__init__.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/services/taskloop/collectors.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/services/taskloop/engine.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/services/taskloop/requirements.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/services/trace.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/services/versions.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/storage/__init__.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/storage/artifacts.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/storage/db.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/storage/graph_impacts.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/storage/migrations/0001_init.sql +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/storage/migrations/0002_versions.sql +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/storage/migrations/0003_graph.sql +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/storage/migrations/0004_taskloop.sql +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/storage/migrations/0005_project_brief.sql +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/storage/repositories.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/storage/tasks.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/storage/versions.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/README.md +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/index.html +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/package-lock.json +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/package.json +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/components/Async.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/components/AttemptCard.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/components/CommandPalette.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/components/DiffView.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/components/ErrorBoundary.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/components/Icon.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/components/Logo.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/components/Sparkline.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/components/TopBar.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/components/bits.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/components/charts.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/components/primitives.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/lib/format.ts +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/lib/status.ts +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/lib/useDebounced.ts +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/main.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/routes/Compare.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/routes/FeatureDetail.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/routes/Features.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/routes/Intelligence.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/routes/Memory.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/routes/NotFound.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/routes/Overview.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/routes/SafeToChange.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/routes/Search.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/routes/TaskDetail.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/routes/Tasks.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/routes/Timeline.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/routes/VersionDetail.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/theme/ThemeProvider.tsx +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/theme/theme.css +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/tsconfig.json +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/vite.config.ts +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/__init__.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/conftest.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_agent_context.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_analysis.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_analytics.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_api_tasks.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_artifacts_restore.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_checkpoint_phase6.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_checkpoint_pipeline.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_cli.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_cli_checkpoint.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_cli_compare_search.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_cli_init_status.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_collectors.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_config.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_databricks.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_doctor.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_dotenv.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_e2e.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_entire_adapter.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_entire_checkpoints.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_entire_real.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_errors.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_git_adapter.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_graph_impact.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_init_service.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_logging.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_mcp.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_memory.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_migrations.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_paths.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_taskloop.py +0 -0
- {devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/tests/test_versions.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: devmemory-cli
|
|
3
|
-
Version: 0.1.0.
|
|
3
|
+
Version: 0.1.0.dev1
|
|
4
4
|
Summary: Development-memory and version-intelligence for AI-assisted software development
|
|
5
5
|
Project-URL: Documentation, https://github.com/KYadavPR/devmemory
|
|
6
6
|
Project-URL: Source, https://github.com/KYadavPR/devmemory
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Databricks Genie - a natural-language chat layer over the published telemetry.
|
|
2
|
+
|
|
3
|
+
Genie answers questions in plain English against a *Genie space* that sits on
|
|
4
|
+
top of the ``devmemory.analytics`` Delta tables (see
|
|
5
|
+
:mod:`devmemory.adapters.databricks`). This adapter is a thin wrapper over the
|
|
6
|
+
Databricks SDK's Genie Conversation API: one call per turn, returning the
|
|
7
|
+
answer text, the SQL Genie ran, and a small preview of the rows.
|
|
8
|
+
|
|
9
|
+
Everything degrades gracefully (§21): with no credentials, no ``databricks``
|
|
10
|
+
extra, or no space id, :attr:`GenieAdapter.is_configured` is ``False`` and the
|
|
11
|
+
dashboard simply hides the chat.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import os
|
|
17
|
+
from typing import TYPE_CHECKING, Any
|
|
18
|
+
|
|
19
|
+
from pydantic import BaseModel
|
|
20
|
+
|
|
21
|
+
from devmemory.config import DevMemoryConfig, resolve_databricks_credentials
|
|
22
|
+
from devmemory.domain.errors import DatabricksError
|
|
23
|
+
from devmemory.logging import get_logger
|
|
24
|
+
|
|
25
|
+
if TYPE_CHECKING:
|
|
26
|
+
from databricks.sdk import WorkspaceClient
|
|
27
|
+
|
|
28
|
+
_log = get_logger(__name__)
|
|
29
|
+
|
|
30
|
+
_MAX_ROWS = 50
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class GenieUnavailableError(DatabricksError):
|
|
34
|
+
"""Genie is not configured, the SDK is missing, or the space is unreachable."""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
class GenieAnswer(BaseModel):
|
|
38
|
+
conversation_id: str | None = None
|
|
39
|
+
message_id: str | None = None
|
|
40
|
+
question: str
|
|
41
|
+
text: str | None = None
|
|
42
|
+
sql: str | None = None
|
|
43
|
+
sql_description: str | None = None
|
|
44
|
+
columns: list[str] = []
|
|
45
|
+
rows: list[list[Any]] = []
|
|
46
|
+
row_count: int | None = None
|
|
47
|
+
truncated: bool = False
|
|
48
|
+
error: str | None = None
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class GenieAdapter:
|
|
52
|
+
def __init__(self, config: DevMemoryConfig) -> None:
|
|
53
|
+
self._config = config
|
|
54
|
+
self._space_id = (
|
|
55
|
+
os.environ.get("DATABRICKS_GENIE_SPACE_ID") or config.databricks.genie_space_id or ""
|
|
56
|
+
).strip()
|
|
57
|
+
self._client: WorkspaceClient | None = None
|
|
58
|
+
|
|
59
|
+
@property
|
|
60
|
+
def space_id(self) -> str:
|
|
61
|
+
return self._space_id
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def is_configured(self) -> bool:
|
|
65
|
+
return bool(self._space_id) and resolve_databricks_credentials() is not None
|
|
66
|
+
|
|
67
|
+
def unavailable_reason(self) -> str | None:
|
|
68
|
+
if not self._space_id:
|
|
69
|
+
return "no Genie space configured (set databricks.genie_space_id or DATABRICKS_GENIE_SPACE_ID)"
|
|
70
|
+
if resolve_databricks_credentials() is None:
|
|
71
|
+
return "Databricks credentials are not set (DATABRICKS_HOST / DATABRICKS_TOKEN)"
|
|
72
|
+
return None
|
|
73
|
+
|
|
74
|
+
# -- connection ---------------------------------------------------
|
|
75
|
+
|
|
76
|
+
def _workspace(self) -> WorkspaceClient:
|
|
77
|
+
if self._client is not None:
|
|
78
|
+
return self._client
|
|
79
|
+
creds = resolve_databricks_credentials()
|
|
80
|
+
if creds is None:
|
|
81
|
+
raise GenieUnavailableError("Databricks credentials are not set.")
|
|
82
|
+
try:
|
|
83
|
+
from databricks.sdk import WorkspaceClient
|
|
84
|
+
except ImportError as exc: # pragma: no cover - extra not installed
|
|
85
|
+
raise GenieUnavailableError(
|
|
86
|
+
"the databricks extra is not installed",
|
|
87
|
+
hint="pip install 'devmemory-cli[databricks]'",
|
|
88
|
+
) from exc
|
|
89
|
+
self._client = WorkspaceClient(host=creds.host, token=creds.token)
|
|
90
|
+
return self._client
|
|
91
|
+
|
|
92
|
+
# -- one chat turn ----------------------------------------------
|
|
93
|
+
|
|
94
|
+
def ask(self, question: str, *, conversation_id: str | None = None) -> GenieAnswer:
|
|
95
|
+
"""Send one question and wait for the answer. Pass ``conversation_id`` to
|
|
96
|
+
continue an existing thread (Genie keeps the earlier context)."""
|
|
97
|
+
if not self._space_id:
|
|
98
|
+
raise GenieUnavailableError("no Genie space configured")
|
|
99
|
+
w = self._workspace()
|
|
100
|
+
q = question.strip()
|
|
101
|
+
try:
|
|
102
|
+
if conversation_id:
|
|
103
|
+
msg = w.genie.create_message_and_wait(self._space_id, conversation_id, q)
|
|
104
|
+
else:
|
|
105
|
+
msg = w.genie.start_conversation_and_wait(self._space_id, q)
|
|
106
|
+
except Exception as exc:
|
|
107
|
+
raise GenieUnavailableError(f"Genie request failed: {exc}") from exc
|
|
108
|
+
|
|
109
|
+
answer = GenieAnswer(
|
|
110
|
+
question=q,
|
|
111
|
+
conversation_id=msg.conversation_id,
|
|
112
|
+
message_id=msg.message_id,
|
|
113
|
+
error=msg.error.error if msg.error is not None else None,
|
|
114
|
+
)
|
|
115
|
+
self._fill_from_attachments(w, msg, answer)
|
|
116
|
+
_log.info(
|
|
117
|
+
"genie.answer",
|
|
118
|
+
space=self._space_id,
|
|
119
|
+
conversation=answer.conversation_id,
|
|
120
|
+
has_sql=answer.sql is not None,
|
|
121
|
+
rows=len(answer.rows),
|
|
122
|
+
)
|
|
123
|
+
return answer
|
|
124
|
+
|
|
125
|
+
def _fill_from_attachments(
|
|
126
|
+
self, w: WorkspaceClient, msg: Any, answer: GenieAnswer
|
|
127
|
+
) -> None:
|
|
128
|
+
for att in msg.attachments or []:
|
|
129
|
+
if getattr(att, "text", None) and att.text.content:
|
|
130
|
+
answer.text = (answer.text or "") + att.text.content
|
|
131
|
+
query = getattr(att, "query", None)
|
|
132
|
+
if query is None:
|
|
133
|
+
continue
|
|
134
|
+
answer.sql = query.query
|
|
135
|
+
answer.sql_description = query.description
|
|
136
|
+
if not answer.text and query.description:
|
|
137
|
+
answer.text = query.description
|
|
138
|
+
try:
|
|
139
|
+
self._attach_rows(w, msg, att.attachment_id, answer)
|
|
140
|
+
except Exception as exc: # results are best-effort
|
|
141
|
+
_log.warning("genie.results_failed", error=str(exc))
|
|
142
|
+
|
|
143
|
+
def _attach_rows(
|
|
144
|
+
self, w: WorkspaceClient, msg: Any, attachment_id: str, answer: GenieAnswer
|
|
145
|
+
) -> None:
|
|
146
|
+
res = w.genie.get_message_query_result_by_attachment(
|
|
147
|
+
self._space_id, msg.conversation_id, msg.message_id, attachment_id
|
|
148
|
+
)
|
|
149
|
+
sr = res.statement_response
|
|
150
|
+
if sr is None:
|
|
151
|
+
return
|
|
152
|
+
schema = sr.manifest.schema if sr.manifest else None
|
|
153
|
+
cols = (schema.columns or []) if schema else []
|
|
154
|
+
answer.columns = [c.name for c in cols if c.name]
|
|
155
|
+
data = (sr.result.data_array if sr.result else None) or []
|
|
156
|
+
answer.row_count = len(data)
|
|
157
|
+
answer.truncated = len(data) > _MAX_ROWS
|
|
158
|
+
answer.rows = [list(r) for r in data[:_MAX_ROWS]]
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
__all__ = ["GenieAdapter", "GenieAnswer", "GenieUnavailableError"]
|
|
@@ -17,12 +17,15 @@ from fastapi.responses import FileResponse, PlainTextResponse
|
|
|
17
17
|
from fastapi.staticfiles import StaticFiles
|
|
18
18
|
|
|
19
19
|
from devmemory.__about__ import __version__
|
|
20
|
+
from devmemory.adapters.genie import GenieAdapter, GenieAnswer, GenieUnavailableError
|
|
20
21
|
from devmemory.adapters.graph import GraphImpact
|
|
21
22
|
from devmemory.api import mappers
|
|
22
23
|
from devmemory.api.schemas import (
|
|
23
24
|
AgentCheckRequest,
|
|
24
25
|
ComparisonResponse,
|
|
25
26
|
FeatureDetail,
|
|
27
|
+
GenieAskRequest,
|
|
28
|
+
GenieStatus,
|
|
26
29
|
IssueRequest,
|
|
27
30
|
ProjectBriefDoc,
|
|
28
31
|
ProjectBriefRequest,
|
|
@@ -153,6 +156,30 @@ def create_app(repo_path: Path | str | None = None, *, enable_restore: bool = Fa
|
|
|
153
156
|
doc = briefsvc.set_brief(ctx, body.content)
|
|
154
157
|
return ProjectBriefDoc(content=doc.content, updated_at=doc.updated_at)
|
|
155
158
|
|
|
159
|
+
# -- Genie chat (Databricks) -------------------------------------
|
|
160
|
+
|
|
161
|
+
@app.get("/api/genie/status", response_model=GenieStatus)
|
|
162
|
+
def genie_status(ctx: Ctx) -> GenieStatus:
|
|
163
|
+
g = GenieAdapter(ctx.config)
|
|
164
|
+
return GenieStatus(
|
|
165
|
+
configured=g.is_configured,
|
|
166
|
+
space_id=g.space_id or None,
|
|
167
|
+
reason=g.unavailable_reason(),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
@app.post("/api/genie/ask", response_model=GenieAnswer)
|
|
171
|
+
def genie_ask(ctx: Ctx, body: GenieAskRequest) -> GenieAnswer:
|
|
172
|
+
question = body.question.strip()
|
|
173
|
+
if not question:
|
|
174
|
+
raise HTTPException(status_code=422, detail="question is empty")
|
|
175
|
+
g = GenieAdapter(ctx.config)
|
|
176
|
+
if not g.is_configured:
|
|
177
|
+
raise HTTPException(status_code=503, detail=g.unavailable_reason() or "Genie unavailable")
|
|
178
|
+
try:
|
|
179
|
+
return g.ask(question, conversation_id=body.conversation_id)
|
|
180
|
+
except GenieUnavailableError as exc:
|
|
181
|
+
raise HTTPException(status_code=502, detail=exc.message) from exc
|
|
182
|
+
|
|
156
183
|
# -- versions ------------------------------------------------------
|
|
157
184
|
|
|
158
185
|
@app.get("/api/versions", response_model=list[VersionListItem])
|
|
@@ -181,11 +181,26 @@ class ProjectBriefRequest(BaseModel):
|
|
|
181
181
|
content: str
|
|
182
182
|
|
|
183
183
|
|
|
184
|
+
class GenieAskRequest(BaseModel):
|
|
185
|
+
"""One turn in the dashboard's Genie chat."""
|
|
186
|
+
|
|
187
|
+
question: str
|
|
188
|
+
conversation_id: str | None = None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class GenieStatus(BaseModel):
|
|
192
|
+
configured: bool
|
|
193
|
+
space_id: str | None = None
|
|
194
|
+
reason: str | None = None
|
|
195
|
+
|
|
196
|
+
|
|
184
197
|
__all__ = [
|
|
185
198
|
"AgentCheckRequest",
|
|
186
199
|
"ComparisonResponse",
|
|
187
200
|
"FeatureDetail",
|
|
188
201
|
"FeatureHistoryPoint",
|
|
202
|
+
"GenieAskRequest",
|
|
203
|
+
"GenieStatus",
|
|
189
204
|
"IssueRequest",
|
|
190
205
|
"MetricChange",
|
|
191
206
|
"ProjectBriefDoc",
|
|
@@ -101,6 +101,11 @@ class DatabricksSettings(_Section):
|
|
|
101
101
|
enabled: bool = False
|
|
102
102
|
catalog: str = "devmemory"
|
|
103
103
|
schema_name: str = Field(default="analytics", alias="schema")
|
|
104
|
+
genie_space_id: str | None = Field(
|
|
105
|
+
default=None,
|
|
106
|
+
description="Genie space that backs the dashboard chat. "
|
|
107
|
+
"Overridden by DATABRICKS_GENIE_SPACE_ID.",
|
|
108
|
+
)
|
|
104
109
|
|
|
105
110
|
|
|
106
111
|
class RegressionSettings(_Section):
|
{devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/App.tsx
RENAMED
|
@@ -5,6 +5,7 @@ import { TopBar } from "@/components/TopBar";
|
|
|
5
5
|
import { CommandPalette } from "@/components/CommandPalette";
|
|
6
6
|
import { ErrorBoundary } from "@/components/ErrorBoundary";
|
|
7
7
|
import { Overview } from "@/routes/Overview";
|
|
8
|
+
import { Ask } from "@/routes/Ask";
|
|
8
9
|
import { Tasks } from "@/routes/Tasks";
|
|
9
10
|
import { TaskDetail } from "@/routes/TaskDetail";
|
|
10
11
|
import { Timeline } from "@/routes/Timeline";
|
|
@@ -54,6 +55,7 @@ export function App() {
|
|
|
54
55
|
<div className="route-fade" key={location.pathname}>
|
|
55
56
|
<Routes>
|
|
56
57
|
<Route path="/" element={<Overview />} />
|
|
58
|
+
<Route path="/ask" element={<Ask />} />
|
|
57
59
|
<Route path="/tasks" element={<Tasks />} />
|
|
58
60
|
<Route path="/task/:id" element={<TaskDetail />} />
|
|
59
61
|
<Route path="/timeline" element={<Timeline />} />
|
{devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/api/client.ts
RENAMED
|
@@ -16,6 +16,8 @@ import type {
|
|
|
16
16
|
TaskSummary,
|
|
17
17
|
SnapshotSummary,
|
|
18
18
|
ProjectBriefDoc,
|
|
19
|
+
GenieStatus,
|
|
20
|
+
GenieAnswer,
|
|
19
21
|
} from "./types";
|
|
20
22
|
|
|
21
23
|
const BASE = "/api";
|
|
@@ -157,6 +159,22 @@ export const useSearch = (q: string) =>
|
|
|
157
159
|
export const checkChange = (files: string[], intent: string, feature?: string) =>
|
|
158
160
|
apiPost<ChangeGuidance>("/agent/check", { files, intent: intent || null, feature: feature || null });
|
|
159
161
|
|
|
162
|
+
// --- Genie chat (Databricks) ----------------------------------------------
|
|
163
|
+
|
|
164
|
+
export const useGenieStatus = (opts?: QOpts<GenieStatus>) =>
|
|
165
|
+
useQuery({
|
|
166
|
+
queryKey: ["genie-status"],
|
|
167
|
+
queryFn: () => apiGet<GenieStatus>("/genie/status"),
|
|
168
|
+
staleTime: 60_000,
|
|
169
|
+
...opts,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
export const askGenie = (question: string, conversationId: string | null) =>
|
|
173
|
+
apiPost<GenieAnswer>("/genie/ask", {
|
|
174
|
+
question,
|
|
175
|
+
conversation_id: conversationId,
|
|
176
|
+
});
|
|
177
|
+
|
|
160
178
|
// --- project brief (single source of truth) --------------------------------
|
|
161
179
|
|
|
162
180
|
export const useProjectBrief = (opts?: QOpts<ProjectBriefDoc>) =>
|
{devmemory_cli-0.1.0.dev0 → devmemory_cli-0.1.0.dev1}/src/devmemory/web/frontend/src/api/types.ts
RENAMED
|
@@ -520,3 +520,23 @@ export interface ProjectBriefDoc {
|
|
|
520
520
|
content: string;
|
|
521
521
|
updated_at: string | null;
|
|
522
522
|
}
|
|
523
|
+
|
|
524
|
+
export interface GenieStatus {
|
|
525
|
+
configured: boolean;
|
|
526
|
+
space_id: string | null;
|
|
527
|
+
reason: string | null;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export interface GenieAnswer {
|
|
531
|
+
conversation_id: string | null;
|
|
532
|
+
message_id: string | null;
|
|
533
|
+
question: string;
|
|
534
|
+
text: string | null;
|
|
535
|
+
sql: string | null;
|
|
536
|
+
sql_description: string | null;
|
|
537
|
+
columns: string[];
|
|
538
|
+
rows: unknown[][];
|
|
539
|
+
row_count: number | null;
|
|
540
|
+
truncated: boolean;
|
|
541
|
+
error: string | null;
|
|
542
|
+
}
|
|
@@ -5,6 +5,7 @@ import { Logo } from "./Logo";
|
|
|
5
5
|
|
|
6
6
|
const NAV: { to: string; label: string; icon: IconName }[] = [
|
|
7
7
|
{ to: "/", label: "Overview", icon: "overview" },
|
|
8
|
+
{ to: "/ask", label: "Ask", icon: "sparkles" },
|
|
8
9
|
{ to: "/tasks", label: "Tasks", icon: "target" },
|
|
9
10
|
{ to: "/timeline", label: "Timeline", icon: "timeline" },
|
|
10
11
|
{ to: "/features", label: "Features", icon: "features" },
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from "react";
|
|
2
|
+
import { useMutation } from "@tanstack/react-query";
|
|
3
|
+
import { useGenieStatus, askGenie } from "@/api/client";
|
|
4
|
+
import { Card, PageHead, EmptyState } from "@/components/primitives";
|
|
5
|
+
import { Icon } from "@/components/Icon";
|
|
6
|
+
import type { GenieAnswer } from "@/api/types";
|
|
7
|
+
|
|
8
|
+
type Turn =
|
|
9
|
+
| { role: "user"; text: string }
|
|
10
|
+
| { role: "genie"; answer: GenieAnswer }
|
|
11
|
+
| { role: "error"; text: string };
|
|
12
|
+
|
|
13
|
+
const SUGGESTIONS = [
|
|
14
|
+
"Which features regressed the most?",
|
|
15
|
+
"Show every version that failed tests, newest first",
|
|
16
|
+
"What files change together most often?",
|
|
17
|
+
"How did quote_latency_ms move over time?",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function errorDetail(message: string): string {
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(message);
|
|
23
|
+
if (parsed && typeof parsed.detail === "string") return parsed.detail;
|
|
24
|
+
} catch {
|
|
25
|
+
/* not json */
|
|
26
|
+
}
|
|
27
|
+
return message;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function Ask() {
|
|
31
|
+
const status = useGenieStatus();
|
|
32
|
+
const [turns, setTurns] = useState<Turn[]>([]);
|
|
33
|
+
const [draft, setDraft] = useState("");
|
|
34
|
+
const [conversationId, setConversationId] = useState<string | null>(null);
|
|
35
|
+
const scrollerRef = useRef<HTMLDivElement>(null);
|
|
36
|
+
|
|
37
|
+
const ask = useMutation({
|
|
38
|
+
mutationFn: (q: string) => askGenie(q, conversationId),
|
|
39
|
+
onSuccess: (answer) => {
|
|
40
|
+
if (answer.conversation_id) setConversationId(answer.conversation_id);
|
|
41
|
+
setTurns((t) => [...t, { role: "genie", answer }]);
|
|
42
|
+
},
|
|
43
|
+
onError: (err: Error) => {
|
|
44
|
+
setTurns((t) => [...t, { role: "error", text: errorDetail(err.message) }]);
|
|
45
|
+
},
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
scrollerRef.current?.scrollTo({ top: scrollerRef.current.scrollHeight, behavior: "smooth" });
|
|
50
|
+
}, [turns, ask.isPending]);
|
|
51
|
+
|
|
52
|
+
const send = (q: string) => {
|
|
53
|
+
const question = q.trim();
|
|
54
|
+
if (!question || ask.isPending) return;
|
|
55
|
+
setTurns((t) => [...t, { role: "user", text: question }]);
|
|
56
|
+
setDraft("");
|
|
57
|
+
ask.mutate(question);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const newChat = () => {
|
|
61
|
+
setTurns([]);
|
|
62
|
+
setConversationId(null);
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
return (
|
|
66
|
+
<>
|
|
67
|
+
<PageHead
|
|
68
|
+
title="Ask"
|
|
69
|
+
subtitle="Natural-language questions over your development history, answered by Databricks Genie against the devmemory.analytics tables."
|
|
70
|
+
actions={
|
|
71
|
+
turns.length > 0 ? (
|
|
72
|
+
<button className="btn" onClick={newChat}>
|
|
73
|
+
<Icon name="plus" size={14} /> New chat
|
|
74
|
+
</button>
|
|
75
|
+
) : undefined
|
|
76
|
+
}
|
|
77
|
+
/>
|
|
78
|
+
|
|
79
|
+
{status.data && !status.data.configured ? (
|
|
80
|
+
<Card title="Genie is not configured" pad>
|
|
81
|
+
<p className="text-sm">{status.data.reason}</p>
|
|
82
|
+
<p className="muted text-sm">
|
|
83
|
+
Set a Genie space that reads the <code>devmemory.analytics</code> schema, then point
|
|
84
|
+
DevMemory at it:
|
|
85
|
+
</p>
|
|
86
|
+
<pre className="loop-prompt">{`# .devmemory/config.json
|
|
87
|
+
"databricks": { "enabled": true, "genie_space_id": "01ef..." }
|
|
88
|
+
|
|
89
|
+
# or the environment
|
|
90
|
+
DATABRICKS_HOST=... DATABRICKS_TOKEN=...
|
|
91
|
+
DATABRICKS_GENIE_SPACE_ID=01ef...`}</pre>
|
|
92
|
+
<p className="muted text-xs">
|
|
93
|
+
Run <code>devmemory databricks sync</code> first so the tables have data for Genie to
|
|
94
|
+
query.
|
|
95
|
+
</p>
|
|
96
|
+
</Card>
|
|
97
|
+
) : (
|
|
98
|
+
<Card flush>
|
|
99
|
+
<div className="chat" ref={scrollerRef}>
|
|
100
|
+
{turns.length === 0 && (
|
|
101
|
+
<div className="chat__empty">
|
|
102
|
+
<EmptyState
|
|
103
|
+
icon="sparkles"
|
|
104
|
+
title="Ask about your project"
|
|
105
|
+
sub="Genie writes the SQL, runs it on Databricks, and explains the result."
|
|
106
|
+
/>
|
|
107
|
+
<div className="chat__suggest">
|
|
108
|
+
{SUGGESTIONS.map((s) => (
|
|
109
|
+
<button key={s} className="chip" onClick={() => send(s)}>
|
|
110
|
+
{s}
|
|
111
|
+
</button>
|
|
112
|
+
))}
|
|
113
|
+
</div>
|
|
114
|
+
</div>
|
|
115
|
+
)}
|
|
116
|
+
|
|
117
|
+
{turns.map((turn, i) => (
|
|
118
|
+
<TurnView key={i} turn={turn} />
|
|
119
|
+
))}
|
|
120
|
+
|
|
121
|
+
{ask.isPending && (
|
|
122
|
+
<div className="chat__msg chat__msg--genie">
|
|
123
|
+
<div className="chat__role">Genie</div>
|
|
124
|
+
<div className="chat__body row" style={{ gap: 8 }}>
|
|
125
|
+
<span className="spinner" /> thinking…
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
)}
|
|
129
|
+
</div>
|
|
130
|
+
|
|
131
|
+
<form
|
|
132
|
+
className="chat__input"
|
|
133
|
+
onSubmit={(e) => {
|
|
134
|
+
e.preventDefault();
|
|
135
|
+
send(draft);
|
|
136
|
+
}}
|
|
137
|
+
>
|
|
138
|
+
<input
|
|
139
|
+
className="input"
|
|
140
|
+
placeholder="Ask a question about this project…"
|
|
141
|
+
value={draft}
|
|
142
|
+
onChange={(e) => setDraft(e.target.value)}
|
|
143
|
+
disabled={ask.isPending || !status.data?.configured}
|
|
144
|
+
/>
|
|
145
|
+
<button
|
|
146
|
+
className="btn btn--primary"
|
|
147
|
+
type="submit"
|
|
148
|
+
disabled={ask.isPending || !draft.trim() || !status.data?.configured}
|
|
149
|
+
>
|
|
150
|
+
<Icon name="arrowRight" size={15} />
|
|
151
|
+
</button>
|
|
152
|
+
</form>
|
|
153
|
+
</Card>
|
|
154
|
+
)}
|
|
155
|
+
</>
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function TurnView({ turn }: { turn: Turn }) {
|
|
160
|
+
if (turn.role === "user") {
|
|
161
|
+
return (
|
|
162
|
+
<div className="chat__msg chat__msg--user">
|
|
163
|
+
<div className="chat__body">{turn.text}</div>
|
|
164
|
+
</div>
|
|
165
|
+
);
|
|
166
|
+
}
|
|
167
|
+
if (turn.role === "error") {
|
|
168
|
+
return (
|
|
169
|
+
<div className="chat__msg chat__msg--genie">
|
|
170
|
+
<div className="chat__role">Genie</div>
|
|
171
|
+
<div className="chat__body callout callout--bad" style={{ padding: "8px 10px" }}>
|
|
172
|
+
<Icon name="alert" size={13} /> {turn.text}
|
|
173
|
+
</div>
|
|
174
|
+
</div>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const a = turn.answer;
|
|
179
|
+
return (
|
|
180
|
+
<div className="chat__msg chat__msg--genie">
|
|
181
|
+
<div className="chat__role">Genie</div>
|
|
182
|
+
<div className="chat__body stack" style={{ gap: 10 }}>
|
|
183
|
+
{a.error && (
|
|
184
|
+
<div className="callout callout--bad" style={{ padding: "8px 10px" }}>
|
|
185
|
+
<Icon name="alert" size={13} /> {a.error}
|
|
186
|
+
</div>
|
|
187
|
+
)}
|
|
188
|
+
{a.text && <div style={{ whiteSpace: "pre-wrap" }}>{a.text}</div>}
|
|
189
|
+
{a.rows.length > 0 && <ResultTable columns={a.columns} rows={a.rows} />}
|
|
190
|
+
{(a.row_count != null || a.truncated) && (
|
|
191
|
+
<div className="muted text-xs">
|
|
192
|
+
{a.row_count ?? a.rows.length} row{(a.row_count ?? a.rows.length) === 1 ? "" : "s"}
|
|
193
|
+
{a.truncated && ` · showing first ${a.rows.length}`}
|
|
194
|
+
</div>
|
|
195
|
+
)}
|
|
196
|
+
{a.sql && (
|
|
197
|
+
<details className="chat__sql">
|
|
198
|
+
<summary>SQL</summary>
|
|
199
|
+
<pre className="loop-prompt">{a.sql}</pre>
|
|
200
|
+
</details>
|
|
201
|
+
)}
|
|
202
|
+
{!a.text && !a.rows.length && !a.sql && !a.error && (
|
|
203
|
+
<div className="muted">No answer returned.</div>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function ResultTable({ columns, rows }: { columns: string[]; rows: unknown[][] }) {
|
|
211
|
+
return (
|
|
212
|
+
<div className="dm-table__scroll">
|
|
213
|
+
<table className="dm-table">
|
|
214
|
+
<thead>
|
|
215
|
+
<tr>
|
|
216
|
+
{columns.map((c) => (
|
|
217
|
+
<th key={c}>{c}</th>
|
|
218
|
+
))}
|
|
219
|
+
</tr>
|
|
220
|
+
</thead>
|
|
221
|
+
<tbody>
|
|
222
|
+
{rows.map((r, i) => (
|
|
223
|
+
<tr key={i}>
|
|
224
|
+
{r.map((cell, j) => (
|
|
225
|
+
<td key={j} className="mono text-xs">
|
|
226
|
+
{cell == null ? <span className="muted">null</span> : String(cell)}
|
|
227
|
+
</td>
|
|
228
|
+
))}
|
|
229
|
+
</tr>
|
|
230
|
+
))}
|
|
231
|
+
</tbody>
|
|
232
|
+
</table>
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
@@ -965,3 +965,93 @@ textarea.input {
|
|
|
965
965
|
max-height: 320px;
|
|
966
966
|
overflow-y: auto;
|
|
967
967
|
}
|
|
968
|
+
|
|
969
|
+
/* Genie chat ("Ask") */
|
|
970
|
+
.chat {
|
|
971
|
+
display: flex;
|
|
972
|
+
flex-direction: column;
|
|
973
|
+
gap: 14px;
|
|
974
|
+
padding: 18px;
|
|
975
|
+
min-height: 340px;
|
|
976
|
+
max-height: calc(100vh - 300px);
|
|
977
|
+
overflow-y: auto;
|
|
978
|
+
}
|
|
979
|
+
.chat__empty {
|
|
980
|
+
margin: auto;
|
|
981
|
+
display: flex;
|
|
982
|
+
flex-direction: column;
|
|
983
|
+
align-items: center;
|
|
984
|
+
gap: 16px;
|
|
985
|
+
}
|
|
986
|
+
.chat__suggest {
|
|
987
|
+
display: flex;
|
|
988
|
+
flex-wrap: wrap;
|
|
989
|
+
gap: 8px;
|
|
990
|
+
justify-content: center;
|
|
991
|
+
max-width: 560px;
|
|
992
|
+
}
|
|
993
|
+
.chip {
|
|
994
|
+
border: 1px solid var(--border);
|
|
995
|
+
background: var(--surface);
|
|
996
|
+
color: var(--text);
|
|
997
|
+
border-radius: 999px;
|
|
998
|
+
padding: 6px 12px;
|
|
999
|
+
font-size: 0.8125rem;
|
|
1000
|
+
cursor: pointer;
|
|
1001
|
+
}
|
|
1002
|
+
.chip:hover {
|
|
1003
|
+
border-color: var(--accent);
|
|
1004
|
+
color: var(--accent);
|
|
1005
|
+
}
|
|
1006
|
+
.chat__msg {
|
|
1007
|
+
display: flex;
|
|
1008
|
+
flex-direction: column;
|
|
1009
|
+
gap: 4px;
|
|
1010
|
+
max-width: 82%;
|
|
1011
|
+
}
|
|
1012
|
+
.chat__msg--user {
|
|
1013
|
+
align-self: flex-end;
|
|
1014
|
+
align-items: flex-end;
|
|
1015
|
+
}
|
|
1016
|
+
.chat__msg--genie {
|
|
1017
|
+
align-self: flex-start;
|
|
1018
|
+
max-width: 100%;
|
|
1019
|
+
width: 100%;
|
|
1020
|
+
}
|
|
1021
|
+
.chat__role {
|
|
1022
|
+
font-size: 0.6875rem;
|
|
1023
|
+
text-transform: uppercase;
|
|
1024
|
+
letter-spacing: 0.06em;
|
|
1025
|
+
color: var(--text-muted);
|
|
1026
|
+
}
|
|
1027
|
+
.chat__msg--user .chat__body {
|
|
1028
|
+
background: var(--accent);
|
|
1029
|
+
color: #fff;
|
|
1030
|
+
border-radius: var(--r-2);
|
|
1031
|
+
padding: 8px 12px;
|
|
1032
|
+
white-space: pre-wrap;
|
|
1033
|
+
}
|
|
1034
|
+
.chat__msg--genie .chat__body {
|
|
1035
|
+
background: var(--surface-2, var(--surface));
|
|
1036
|
+
border: 1px solid var(--border);
|
|
1037
|
+
border-radius: var(--r-2);
|
|
1038
|
+
padding: 12px 14px;
|
|
1039
|
+
font-size: 0.9375rem;
|
|
1040
|
+
}
|
|
1041
|
+
.chat__sql summary {
|
|
1042
|
+
cursor: pointer;
|
|
1043
|
+
font-size: 0.8125rem;
|
|
1044
|
+
color: var(--text-muted);
|
|
1045
|
+
}
|
|
1046
|
+
.chat__sql[open] summary {
|
|
1047
|
+
margin-bottom: 8px;
|
|
1048
|
+
}
|
|
1049
|
+
.chat__input {
|
|
1050
|
+
display: flex;
|
|
1051
|
+
gap: 8px;
|
|
1052
|
+
padding: 14px 18px;
|
|
1053
|
+
border-top: 1px solid var(--border);
|
|
1054
|
+
}
|
|
1055
|
+
.chat__input .input {
|
|
1056
|
+
flex: 1;
|
|
1057
|
+
}
|