mirror of
https://github.com/himanshudongre/smriti.git
synced 2026-08-28 05:14:59 +00:00
Add checkpoint review and assumptions field
Separate assumptions from decisions as a first-class checkpoint field. Add review endpoint that surfaces reasoning consistency issues: contradictions, hidden assumptions, resolved questions, and unused entities. Extend draft extraction, prompt context, and compare diff to include assumptions.
This commit is contained in:
parent
97b7845cb5
commit
2f155e8dcc
9 changed files with 326 additions and 25 deletions
|
|
@ -0,0 +1,30 @@
|
|||
"""add_assumptions_to_commits
|
||||
|
||||
Revision ID: b3c4d5e6f7a8
|
||||
Revises: a1b2c3d4e5f6
|
||||
Create Date: 2026-04-04 20:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'b3c4d5e6f7a8'
|
||||
down_revision: Union[str, None] = 'a1b2c3d4e5f6'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
'commits',
|
||||
sa.Column('assumptions', JSONB(), nullable=False, server_default='[]'),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('commits', 'assumptions')
|
||||
|
|
@ -144,6 +144,11 @@ def build_prompt_from_checkpoints(checkpoints: list[CommitModel], recent_message
|
|||
for d in ckpt.decisions:
|
||||
lines.append(f"- {d}")
|
||||
lines.append("")
|
||||
if ckpt.assumptions:
|
||||
lines.append(f"{label} Key Assumptions:")
|
||||
for a in ckpt.assumptions:
|
||||
lines.append(f"- {a}")
|
||||
lines.append("")
|
||||
if ckpt.tasks:
|
||||
lines.append(f"{label} Open Tasks:")
|
||||
for t in ckpt.tasks:
|
||||
|
|
@ -251,6 +256,7 @@ class ManualCommitRequest(BaseModel):
|
|||
summary: str = ""
|
||||
objective: str = ""
|
||||
decisions: list[str] = Field(default_factory=list)
|
||||
assumptions: list[str] = Field(default_factory=list)
|
||||
tasks: list[str] = Field(default_factory=list)
|
||||
open_questions: list[str] = Field(default_factory=list)
|
||||
entities: list[str] = Field(default_factory=list)
|
||||
|
|
@ -266,6 +272,7 @@ class CommitResponse(BaseModel):
|
|||
summary: str
|
||||
objective: str
|
||||
decisions: list
|
||||
assumptions: list
|
||||
tasks: list
|
||||
open_questions: list
|
||||
entities: list
|
||||
|
|
@ -622,6 +629,7 @@ def manual_commit(payload: ManualCommitRequest, db: Session = Depends(get_db)):
|
|||
summary=payload.summary,
|
||||
objective=payload.objective,
|
||||
decisions=payload.decisions,
|
||||
assumptions=payload.assumptions,
|
||||
tasks=payload.tasks,
|
||||
open_questions=payload.open_questions,
|
||||
entities=payload.entities,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
"""Checkpoint routes for auto drafting."""
|
||||
"""Checkpoint routes for auto drafting and review."""
|
||||
|
||||
import json
|
||||
import logging
|
||||
|
|
@ -10,8 +10,8 @@ from sqlalchemy import select
|
|||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.database import get_db
|
||||
from app.db.models import ChatSession, TurnEvent
|
||||
from app.schemas import CheckpointDraftRequest, CheckpointDraftResponse
|
||||
from app.db.models import ChatSession, CommitModel, TurnEvent
|
||||
from app.schemas import CheckpointDraftRequest, CheckpointDraftResponse, CheckpointReviewResponse, ReviewIssue
|
||||
from app.providers.registry import get_adapter
|
||||
from app.config_loader import get_config
|
||||
|
||||
|
|
@ -107,6 +107,7 @@ Return a STRICT JSON object with exactly this schema — no extra keys, no markd
|
|||
"objective": "The main goal the user is working toward in this conversation (1 sentence, or empty string if unclear)",
|
||||
"summary": "Concise narrative of what was discussed and figured out (2-4 sentences)",
|
||||
"decisions": ["An explicit decision made in the conversation", "Another explicit decision"],
|
||||
"assumptions": ["Something taken for granted but not explicitly decided"],
|
||||
"tasks": ["A concrete action item from the conversation", "Another action item"],
|
||||
"open_questions": ["An unresolved question from the conversation"],
|
||||
"entities": ["Key concept, tool, place, or system mentioned"]
|
||||
|
|
@ -114,6 +115,7 @@ Return a STRICT JSON object with exactly this schema — no extra keys, no markd
|
|||
|
||||
Rules:
|
||||
- decisions: only include choices explicitly made in the conversation, not hypothetical ones
|
||||
- assumptions: things the conversation takes for granted that were NOT explicitly debated or decided (e.g., implicit constraints, assumed technology choices, timeline expectations treated as given)
|
||||
- tasks: only include things the user said they will do or need to do
|
||||
- entities: proper nouns and key technical/domain terms only
|
||||
- All arrays may be empty if nothing relevant was discussed
|
||||
|
|
@ -138,14 +140,17 @@ Rules:
|
|||
raw_response = adapter.send(messages, model=bg_model, response_format={"type": "json_object"})
|
||||
data = json.loads(raw_response)
|
||||
|
||||
_dedup = lambda items: list(dict.fromkeys([str(x).strip() for x in items if x]))
|
||||
|
||||
return CheckpointDraftResponse(
|
||||
title=str(data.get("title", "")).strip(),
|
||||
objective=str(data.get("objective", "")).strip(),
|
||||
summary=str(data.get("summary", "")).strip(),
|
||||
decisions=list(dict.fromkeys([str(x).strip() for x in data.get("decisions", []) if x])),
|
||||
tasks=list(dict.fromkeys([str(x).strip() for x in data.get("tasks", []) if x])),
|
||||
open_questions=list(dict.fromkeys([str(x).strip() for x in data.get("open_questions", []) if x])),
|
||||
entities=list(dict.fromkeys([str(x).strip() for x in data.get("entities", []) if x])),
|
||||
decisions=_dedup(data.get("decisions", [])),
|
||||
assumptions=_dedup(data.get("assumptions", [])),
|
||||
tasks=_dedup(data.get("tasks", [])),
|
||||
open_questions=_dedup(data.get("open_questions", [])),
|
||||
entities=_dedup(data.get("entities", [])),
|
||||
)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
|
|
@ -154,3 +159,114 @@ Rules:
|
|||
except Exception as e:
|
||||
logger.error(f"LLM extraction error: {e}")
|
||||
raise HTTPException(status_code=502, detail=f"Drafting failed: {e}")
|
||||
|
||||
|
||||
# ── Review endpoint ──────────────────────────────────────────────────────────
|
||||
|
||||
def _format_bullet_list(items: list, fallback: str = "None listed") -> str:
|
||||
if not items:
|
||||
return fallback
|
||||
return "\n".join(f"- {item}" for item in items)
|
||||
|
||||
|
||||
@router.post("/{checkpoint_id}/review", response_model=CheckpointReviewResponse)
|
||||
def review_checkpoint(checkpoint_id: uuid.UUID, db: Session = Depends(get_db)):
|
||||
"""
|
||||
Review a checkpoint for reasoning consistency.
|
||||
|
||||
Sends the checkpoint's structured fields to the background intelligence
|
||||
provider and returns a small number of high-signal issues.
|
||||
"""
|
||||
commit = db.get(CommitModel, checkpoint_id)
|
||||
if not commit:
|
||||
raise HTTPException(status_code=404, detail="Checkpoint not found")
|
||||
|
||||
prompt = f"""You are a reasoning consistency reviewer.
|
||||
Review this structured checkpoint that captures the state of a reasoning process.
|
||||
|
||||
CHECKPOINT:
|
||||
Title: {commit.message}
|
||||
Objective: {commit.objective or "(not set)"}
|
||||
Summary: {commit.summary or "(not set)"}
|
||||
|
||||
Assumptions:
|
||||
{_format_bullet_list(commit.assumptions or [])}
|
||||
|
||||
Decisions:
|
||||
{_format_bullet_list(commit.decisions or [])}
|
||||
|
||||
Tasks:
|
||||
{_format_bullet_list(commit.tasks or [])}
|
||||
|
||||
Open Questions:
|
||||
{_format_bullet_list(commit.open_questions or [])}
|
||||
|
||||
Entities:
|
||||
{", ".join(commit.entities or []) or "None listed"}
|
||||
|
||||
Identify ONLY these issue types:
|
||||
|
||||
1. CONTRADICTION: Two decisions, or a decision and an assumption, that appear to conflict with each other.
|
||||
2. HIDDEN_ASSUMPTION: Something the summary or decisions clearly rely on that is not listed as an assumption or decision. Only flag when the implicit reliance is obvious.
|
||||
3. RESOLVED_QUESTION: An open question that appears already answered by a decision or the summary.
|
||||
4. UNUSED_ENTITY: An entity that is not referenced in the summary, decisions, tasks, or objective.
|
||||
|
||||
Rules:
|
||||
- Be conservative. Only flag issues you are confident about.
|
||||
- Prefer precision over recall — it is better to miss an issue than to flag a false one.
|
||||
- Reference specific text from the checkpoint in each description.
|
||||
- Return at most 5 issues.
|
||||
- Suggestions should be brief and actionable.
|
||||
- If no issues are found, return empty arrays.
|
||||
|
||||
Return STRICT JSON — no markdown, no explanation:
|
||||
{{
|
||||
"issues": [
|
||||
{{
|
||||
"type": "contradiction | hidden_assumption | resolved_question | unused_entity",
|
||||
"description": "Brief description referencing specific checkpoint text"
|
||||
}}
|
||||
],
|
||||
"suggestions": ["Brief actionable suggestion"]
|
||||
}}"""
|
||||
|
||||
try:
|
||||
cfg = get_config()
|
||||
bg_provider = cfg.background.provider
|
||||
bg_model = cfg.background.model
|
||||
adapter = get_adapter(bg_provider, allow_mock=False)
|
||||
except Exception as e:
|
||||
raise HTTPException(
|
||||
status_code=500,
|
||||
detail=f"Background provider not configured. Error: {e}"
|
||||
)
|
||||
|
||||
try:
|
||||
raw_response = adapter.send(
|
||||
[{"role": "user", "content": prompt}],
|
||||
model=bg_model,
|
||||
response_format={"type": "json_object"},
|
||||
)
|
||||
data = json.loads(raw_response)
|
||||
|
||||
issues = []
|
||||
for item in data.get("issues", []):
|
||||
issue_type = str(item.get("type", "")).strip()
|
||||
desc = str(item.get("description", "")).strip()
|
||||
if issue_type and desc:
|
||||
issues.append(ReviewIssue(type=issue_type, description=desc))
|
||||
|
||||
suggestions = [str(s).strip() for s in data.get("suggestions", []) if s]
|
||||
|
||||
return CheckpointReviewResponse(
|
||||
checkpoint_id=checkpoint_id,
|
||||
issues=issues[:5],
|
||||
suggestions=suggestions[:5],
|
||||
)
|
||||
|
||||
except json.JSONDecodeError:
|
||||
logger.error(f"Review returned invalid JSON: {raw_response}")
|
||||
raise HTTPException(status_code=502, detail="Failed to parse review response.")
|
||||
except Exception as e:
|
||||
logger.error(f"Review error: {e}")
|
||||
raise HTTPException(status_code=502, detail=f"Review failed: {e}")
|
||||
|
|
|
|||
|
|
@ -108,6 +108,7 @@ class CheckpointDetail(BaseModel):
|
|||
summary: str
|
||||
objective: str
|
||||
decisions: list
|
||||
assumptions: list
|
||||
tasks: list
|
||||
open_questions: list
|
||||
|
||||
|
|
@ -120,6 +121,9 @@ class CheckpointDiff(BaseModel):
|
|||
decisions_only_a: list[str]
|
||||
decisions_only_b: list[str]
|
||||
decisions_shared: list[str]
|
||||
assumptions_only_a: list[str] = Field(default_factory=list)
|
||||
assumptions_only_b: list[str] = Field(default_factory=list)
|
||||
assumptions_shared: list[str] = Field(default_factory=list)
|
||||
tasks_only_a: list[str]
|
||||
tasks_only_b: list[str]
|
||||
tasks_shared: list[str]
|
||||
|
|
@ -145,6 +149,7 @@ class ReachableCheckpoint(BaseModel):
|
|||
summary: str
|
||||
objective: str
|
||||
decisions: list
|
||||
assumptions: list
|
||||
tasks: list
|
||||
open_questions: list
|
||||
entities: list
|
||||
|
|
@ -301,6 +306,9 @@ def compare_checkpoints(a_id: uuid.UUID, b_id: uuid.UUID, db: Session = Depends(
|
|||
dec_only_a, dec_only_b, dec_shared = _diff_lists(
|
||||
commit_a.decisions or [], commit_b.decisions or []
|
||||
)
|
||||
assump_only_a, assump_only_b, assump_shared = _diff_lists(
|
||||
commit_a.assumptions or [], commit_b.assumptions or []
|
||||
)
|
||||
task_only_a, task_only_b, task_shared = _diff_lists(
|
||||
commit_a.tasks or [], commit_b.tasks or []
|
||||
)
|
||||
|
|
@ -314,6 +322,7 @@ def compare_checkpoints(a_id: uuid.UUID, b_id: uuid.UUID, db: Session = Depends(
|
|||
summary=c.summary or "",
|
||||
objective=c.objective or "",
|
||||
decisions=[_extract_text(d) for d in (c.decisions or [])],
|
||||
assumptions=[_extract_text(a) for a in (c.assumptions or [])],
|
||||
tasks=[_extract_text(t) for t in (c.tasks or [])],
|
||||
open_questions=[_extract_text(q) for q in (c.open_questions or [])],
|
||||
)
|
||||
|
|
@ -329,6 +338,9 @@ def compare_checkpoints(a_id: uuid.UUID, b_id: uuid.UUID, db: Session = Depends(
|
|||
decisions_only_a=dec_only_a,
|
||||
decisions_only_b=dec_only_b,
|
||||
decisions_shared=dec_shared,
|
||||
assumptions_only_a=assump_only_a,
|
||||
assumptions_only_b=assump_only_b,
|
||||
assumptions_shared=assump_shared,
|
||||
tasks_only_a=task_only_a,
|
||||
tasks_only_b=task_only_b,
|
||||
tasks_shared=task_shared,
|
||||
|
|
|
|||
|
|
@ -169,6 +169,7 @@ class CommitModel(Base):
|
|||
summary: Mapped[str] = mapped_column(Text, default="")
|
||||
objective: Mapped[str] = mapped_column(Text, default="")
|
||||
decisions: Mapped[dict] = mapped_column(JSONB, default=list)
|
||||
assumptions: Mapped[dict] = mapped_column(JSONB, default=list)
|
||||
tasks: Mapped[dict] = mapped_column(JSONB, default=list)
|
||||
open_questions: Mapped[dict] = mapped_column(JSONB, default=list)
|
||||
entities: Mapped[dict] = mapped_column(JSONB, default=list)
|
||||
|
|
|
|||
|
|
@ -110,6 +110,18 @@ class CheckpointDraftResponse(BaseModel):
|
|||
objective: str = ""
|
||||
summary: str = ""
|
||||
decisions: list[str] = Field(default_factory=list)
|
||||
assumptions: list[str] = Field(default_factory=list)
|
||||
tasks: list[str] = Field(default_factory=list)
|
||||
open_questions: list[str] = Field(default_factory=list)
|
||||
entities: list[str] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ReviewIssue(BaseModel):
|
||||
type: str # contradiction, hidden_assumption, resolved_question, unused_entity
|
||||
description: str
|
||||
|
||||
|
||||
class CheckpointReviewResponse(BaseModel):
|
||||
checkpoint_id: uuid.UUID
|
||||
issues: list[ReviewIssue] = Field(default_factory=list)
|
||||
suggestions: list[str] = Field(default_factory=list)
|
||||
|
|
|
|||
|
|
@ -250,6 +250,7 @@ export async function createChatCommit(payload: {
|
|||
summary?: string;
|
||||
objective?: string;
|
||||
decisions?: string[];
|
||||
assumptions?: string[];
|
||||
tasks?: string[];
|
||||
open_questions?: string[];
|
||||
entities?: string[];
|
||||
|
|
@ -322,6 +323,7 @@ export async function draftCheckpoint(payload: {
|
|||
objective: string;
|
||||
summary: string;
|
||||
decisions: string[];
|
||||
assumptions: string[];
|
||||
tasks: string[];
|
||||
open_questions: string[];
|
||||
entities: string[];
|
||||
|
|
@ -331,6 +333,7 @@ export async function draftCheckpoint(payload: {
|
|||
objective: string;
|
||||
summary: string;
|
||||
decisions: string[];
|
||||
assumptions: string[];
|
||||
tasks: string[];
|
||||
open_questions: string[];
|
||||
entities: string[];
|
||||
|
|
@ -354,3 +357,9 @@ export async function getSessionCheckpoints(sessionId: string): Promise<import('
|
|||
return requestV5<import('../types').Commit[]>(`/lineage/sessions/${sessionId}/checkpoints`);
|
||||
}
|
||||
|
||||
export async function reviewCheckpoint(checkpointId: string): Promise<import('../types').CheckpointReviewResponse> {
|
||||
return requestV5<import('../types').CheckpointReviewResponse>(`/checkpoint/${checkpointId}/review`, {
|
||||
method: 'POST',
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,8 +20,9 @@ import {
|
|||
forkSession,
|
||||
compareCheckpoints,
|
||||
getSessionCheckpoints,
|
||||
reviewCheckpoint,
|
||||
} from '../api/client';
|
||||
import type { ChatSession, Commit, CompareResponse, HeadState, TurnEvent, Repo, ProviderStatus } from '../types';
|
||||
import type { ChatSession, Commit, CompareResponse, CheckpointReviewResponse, HeadState, TurnEvent, Repo, ProviderStatus } from '../types';
|
||||
import {
|
||||
Check,
|
||||
Copy,
|
||||
|
|
@ -239,6 +240,7 @@ function CommitModal({ repoId, sessionId, onClose, onCommitted, providerStatus,
|
|||
const [obj, setObj] = useState('');
|
||||
const [tasks, setTasks] = useState('');
|
||||
const [decisions, setDecs] = useState('');
|
||||
const [assumptions, setAssumptions] = useState('');
|
||||
const [openQuestions, setOpenQuestions] = useState('');
|
||||
const [entities, setEntities] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
|
@ -268,6 +270,7 @@ function CommitModal({ repoId, sessionId, onClose, onCommitted, providerStatus,
|
|||
setSummary(draft.summary || '');
|
||||
setTasks((draft.tasks ?? []).join('\n'));
|
||||
setDecs((draft.decisions ?? []).join('\n'));
|
||||
setAssumptions((draft.assumptions ?? []).join('\n'));
|
||||
setOpenQuestions((draft.open_questions ?? []).join('\n'));
|
||||
setEntities((draft.entities ?? []).join('\n'));
|
||||
} catch (e: any) {
|
||||
|
|
@ -290,6 +293,7 @@ function CommitModal({ repoId, sessionId, onClose, onCommitted, providerStatus,
|
|||
objective: obj.trim(),
|
||||
tasks: parseLines(tasks),
|
||||
decisions: parseLines(decisions),
|
||||
assumptions: parseLines(assumptions),
|
||||
open_questions: parseLines(openQuestions),
|
||||
entities: parseLines(entities),
|
||||
});
|
||||
|
|
@ -368,18 +372,6 @@ function CommitModal({ repoId, sessionId, onClose, onCommitted, providerStatus,
|
|||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="text-xs uppercase tracking-wider text-gray-500 block mb-1.5">
|
||||
Tasks (one per line)
|
||||
</label>
|
||||
<textarea
|
||||
className="w-full bg-zinc-900/50 border border-gray-800 rounded-lg px-3 py-2 text-sm text-white outline-none focus:border-gray-500 transition-colors resize-none font-mono"
|
||||
rows={3}
|
||||
placeholder={"Set up database\nWrite tests"}
|
||||
value={tasks}
|
||||
onChange={e => setTasks(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase tracking-wider text-gray-500 block mb-1.5">
|
||||
Decisions (one per line)
|
||||
|
|
@ -392,6 +384,30 @@ function CommitModal({ repoId, sessionId, onClose, onCommitted, providerStatus,
|
|||
onChange={e => setDecs(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase tracking-wider text-gray-500 block mb-1.5">
|
||||
Assumptions (one per line)
|
||||
</label>
|
||||
<textarea
|
||||
className="w-full bg-zinc-900/50 border border-gray-800 rounded-lg px-3 py-2 text-sm text-white outline-none focus:border-gray-500 transition-colors resize-none font-mono"
|
||||
rows={3}
|
||||
placeholder={"Cloud-hosted deployment\nSingle-tenant for now"}
|
||||
value={assumptions}
|
||||
onChange={e => setAssumptions(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase tracking-wider text-gray-500 block mb-1.5">
|
||||
Tasks (one per line)
|
||||
</label>
|
||||
<textarea
|
||||
className="w-full bg-zinc-900/50 border border-gray-800 rounded-lg px-3 py-2 text-sm text-white outline-none focus:border-gray-500 transition-colors resize-none font-mono"
|
||||
rows={3}
|
||||
placeholder={"Set up database\nWrite tests"}
|
||||
value={tasks}
|
||||
onChange={e => setTasks(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs uppercase tracking-wider text-gray-500 block mb-1.5">
|
||||
Open Questions (one per line)
|
||||
|
|
@ -404,14 +420,14 @@ function CommitModal({ repoId, sessionId, onClose, onCommitted, providerStatus,
|
|||
onChange={e => setOpenQuestions(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<div className="col-span-2">
|
||||
<label className="text-xs uppercase tracking-wider text-gray-500 block mb-1.5">
|
||||
Entities (one per line)
|
||||
</label>
|
||||
<textarea
|
||||
className="w-full bg-zinc-900/50 border border-gray-800 rounded-lg px-3 py-2 text-sm text-white outline-none focus:border-gray-500 transition-colors resize-none font-mono"
|
||||
rows={3}
|
||||
placeholder={"Redis\nReact\nPostgreSQL"}
|
||||
rows={2}
|
||||
placeholder={"Redis, React, PostgreSQL"}
|
||||
value={entities}
|
||||
onChange={e => setEntities(e.target.value)}
|
||||
/>
|
||||
|
|
@ -436,7 +452,35 @@ function CommitModal({ repoId, sessionId, onClose, onCommitted, providerStatus,
|
|||
|
||||
// ── CheckpointDetailPanel ─────────────────────────────────────────────────────
|
||||
|
||||
function CheckpointDetailPanel({ commit, onClose }: { commit: Commit; onClose: () => void }) {
|
||||
function CheckpointDetailPanel({ commit, onClose, providerStatus }: { commit: Commit; onClose: () => void; providerStatus: Record<string, ProviderStatus> | null }) {
|
||||
const [reviewResult, setReviewResult] = useState<CheckpointReviewResponse | null>(null);
|
||||
const [reviewing, setReviewing] = useState(false);
|
||||
const [reviewErr, setReviewErr] = useState<string | null>(null);
|
||||
|
||||
const bgConfig = providerStatus?.['background_intelligence'];
|
||||
const bgProviderId = bgConfig?.provider;
|
||||
const isBgValid = bgProviderId ? providerStatus?.[bgProviderId]?.has_key : false;
|
||||
|
||||
const handleReview = async () => {
|
||||
try {
|
||||
setReviewing(true);
|
||||
setReviewErr(null);
|
||||
const result = await reviewCheckpoint(commit.id);
|
||||
setReviewResult(result);
|
||||
} catch (e: any) {
|
||||
setReviewErr(e.message || 'Review failed');
|
||||
} finally {
|
||||
setReviewing(false);
|
||||
}
|
||||
};
|
||||
|
||||
const issueLabel: Record<string, { text: string; classes: string }> = {
|
||||
contradiction: { text: 'Possible contradiction', classes: 'text-red-400 bg-red-500/10 border-red-500/20' },
|
||||
hidden_assumption: { text: 'Hidden assumption', classes: 'text-amber-400 bg-amber-500/10 border-amber-500/20' },
|
||||
resolved_question: { text: 'Possibly resolved', classes: 'text-blue-400 bg-blue-500/10 border-blue-500/20' },
|
||||
unused_entity: { text: 'Possibly unused entity', classes: 'text-gray-400 bg-zinc-800 border-gray-700' },
|
||||
};
|
||||
|
||||
const fmt = (d: string) => new Date(d).toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
|
||||
const rows = (items: string[], label: string) =>
|
||||
items.length > 0 ? (
|
||||
|
|
@ -475,10 +519,56 @@ function CheckpointDetailPanel({ commit, onClose }: { commit: Commit; onClose: (
|
|||
<p className="text-xs text-gray-400 leading-relaxed">{commit.summary}</p>
|
||||
</div>
|
||||
)}
|
||||
{rows(commit.tasks ?? [], 'Tasks')}
|
||||
{rows(commit.decisions ?? [], 'Decisions')}
|
||||
{rows(commit.assumptions ?? [], 'Assumptions')}
|
||||
{rows(commit.tasks ?? [], 'Tasks')}
|
||||
{rows(commit.open_questions ?? [], 'Open Questions')}
|
||||
{rows(commit.entities ?? [], 'Entities')}
|
||||
|
||||
{/* Review checkpoint */}
|
||||
<div className="border-t border-gray-800 pt-3">
|
||||
<button
|
||||
onClick={handleReview}
|
||||
disabled={reviewing || !isBgValid}
|
||||
title={isBgValid ? 'Review this checkpoint for reasoning consistency' : 'Background provider not configured'}
|
||||
className="text-[11px] px-3 py-1.5 rounded-md border border-gray-700 text-gray-400 hover:text-white hover:border-gray-500 transition-colors disabled:opacity-40 flex items-center gap-1.5"
|
||||
>
|
||||
{reviewing ? <Loader2 className="w-3 h-3 animate-spin" /> : <Zap className="w-3 h-3" />}
|
||||
{reviewing ? 'Reviewing…' : reviewResult ? 'Re-review checkpoint' : 'Review checkpoint'}
|
||||
</button>
|
||||
|
||||
{reviewErr && <p className="text-[10px] text-red-400 mt-2">{reviewErr}</p>}
|
||||
|
||||
{reviewResult && (
|
||||
<div className="mt-3 space-y-2">
|
||||
{reviewResult.issues.length === 0 ? (
|
||||
<p className="text-[11px] text-green-400/70">No issues found — reasoning looks consistent.</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{reviewResult.issues.map((issue, i) => {
|
||||
const label = issueLabel[issue.type] || { text: issue.type, classes: 'text-gray-400 bg-zinc-800 border-gray-700' };
|
||||
return (
|
||||
<div key={i} className="space-y-1">
|
||||
<span className={`text-[9px] uppercase tracking-wider px-1.5 py-0.5 rounded border ${label.classes}`}>
|
||||
{label.text}
|
||||
</span>
|
||||
<p className="text-[11px] text-gray-300 leading-relaxed">{issue.description}</p>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{reviewResult.suggestions.length > 0 && (
|
||||
<div className="mt-2">
|
||||
<p className="text-[9px] uppercase tracking-wider text-gray-600 mb-1">Suggestions</p>
|
||||
{reviewResult.suggestions.map((s, i) => (
|
||||
<p key={i} className="text-[10px] text-gray-500 leading-relaxed">→ {s}</p>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -592,6 +682,7 @@ function MemorySpacePanel({
|
|||
forkSourceCommit,
|
||||
mountedCheckpointId,
|
||||
refreshKey,
|
||||
providerStatus,
|
||||
onMount,
|
||||
onFork,
|
||||
onClose,
|
||||
|
|
@ -603,6 +694,7 @@ function MemorySpacePanel({
|
|||
mountedCheckpointId: string | null;
|
||||
/** Increment to force the checkpoint list to reload (e.g. after creating a new checkpoint). */
|
||||
refreshKey: number;
|
||||
providerStatus: Record<string, ProviderStatus> | null;
|
||||
onMount: (info: { id: string; message: string } | null) => void;
|
||||
onFork: (checkpoint: Commit) => void;
|
||||
onClose: () => void;
|
||||
|
|
@ -823,6 +915,7 @@ function MemorySpacePanel({
|
|||
<CheckpointDetailPanel
|
||||
commit={expandedCommit}
|
||||
onClose={() => { setExpandedId(null); setExpandedCommit(null); }}
|
||||
providerStatus={providerStatus}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
|
@ -847,6 +940,7 @@ function MemorySpacePanel({
|
|||
{[
|
||||
{ label: 'Decisions', onlyA: compareResult.diff.decisions_only_a, onlyB: compareResult.diff.decisions_only_b, shared: compareResult.diff.decisions_shared },
|
||||
{ label: 'Tasks', onlyA: compareResult.diff.tasks_only_a, onlyB: compareResult.diff.tasks_only_b, shared: compareResult.diff.tasks_shared },
|
||||
{ label: 'Assumptions', onlyA: compareResult.diff.assumptions_only_a ?? [], onlyB: compareResult.diff.assumptions_only_b ?? [], shared: compareResult.diff.assumptions_shared ?? [] },
|
||||
].map(({ label, onlyA, onlyB, shared }) => (onlyA.length + onlyB.length + shared.length) > 0 && (
|
||||
<div key={label}>
|
||||
<p className="text-[9px] uppercase tracking-wider text-gray-600 mb-1">{label}</p>
|
||||
|
|
@ -1738,6 +1832,7 @@ export function ChatWorkspacePage() {
|
|||
forkSourceCommit={forkSourceCommit}
|
||||
mountedCheckpointId={mountedCheckpointId}
|
||||
refreshKey={checkpointRefreshKey}
|
||||
providerStatus={providerStatus}
|
||||
onMount={info => {
|
||||
if (info !== null) {
|
||||
setMountedCheckpointId(info.id);
|
||||
|
|
|
|||
|
|
@ -103,6 +103,7 @@ export interface Commit {
|
|||
summary: string;
|
||||
objective: string;
|
||||
decisions: string[];
|
||||
assumptions: string[];
|
||||
tasks: string[];
|
||||
open_questions: string[];
|
||||
entities: string[];
|
||||
|
|
@ -215,6 +216,7 @@ export interface CheckpointDetail {
|
|||
summary: string;
|
||||
objective: string;
|
||||
decisions: string[];
|
||||
assumptions: string[];
|
||||
tasks: string[];
|
||||
open_questions: string[];
|
||||
}
|
||||
|
|
@ -227,6 +229,9 @@ export interface CheckpointDiff {
|
|||
decisions_only_a: string[];
|
||||
decisions_only_b: string[];
|
||||
decisions_shared: string[];
|
||||
assumptions_only_a: string[];
|
||||
assumptions_only_b: string[];
|
||||
assumptions_shared: string[];
|
||||
tasks_only_a: string[];
|
||||
tasks_only_b: string[];
|
||||
tasks_shared: string[];
|
||||
|
|
@ -237,3 +242,16 @@ export interface CompareResponse {
|
|||
checkpoint_b: CheckpointDetail;
|
||||
diff: CheckpointDiff;
|
||||
}
|
||||
|
||||
// ── Checkpoint Review Types ──────────────────────────────────────────────────
|
||||
|
||||
export interface ReviewIssue {
|
||||
type: 'contradiction' | 'hidden_assumption' | 'resolved_question' | 'unused_entity';
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface CheckpointReviewResponse {
|
||||
checkpoint_id: string;
|
||||
issues: ReviewIssue[];
|
||||
suggestions: string[];
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue