mirror of
https://github.com/computerim/impactflow-discovery.git
synced 2026-08-27 07:10:37 +00:00
Phase 5: iteration & polish (visualizations, goal history, smart tagging)
Final roadmap phase. No DB migration — it reads data already captured.
- Goal-evolution history: GET /discovery/profile/me/goal-history derives a
per-goal timeline from the profile_revision snapshots (pure aggregator in
app/services/profile_history.py).
- Smart tagging: POST /discovery/integration/suggest-foundation suggests which
foundation a task builds toward + rationale/confidence (FoundationTagger,
app/services/tagging.py). Suggestion only; the person confirms by posting the
task mapping.
- Deeper goal-refinement: the reflect loop accepts an optional focus ("goals")
that steers the coach toward sharpening goals — still mirror, not compass.
- Visualizations: visuals.html renders an Ikigai Venn and an Enneagram diagram
(plain-language callouts, not the raw type number) plus the goal-evolution
timeline; linked from profile.html.
Tests: 99 passing (added pure goal-history tests, goal-history + suggest
endpoint tests, reflect-focus passthrough; run in-container). README updated.
This completes the ImpactFlow Vision roadmap (Phases 1-5) on the Discovery side.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,6 +30,7 @@ from app.services.reflector import (
|
||||
ReflectionCoach,
|
||||
ReflectionError,
|
||||
)
|
||||
from app.services.profile_history import goal_history
|
||||
|
||||
router = APIRouter(prefix="/discovery", tags=["discovery"])
|
||||
|
||||
@@ -383,7 +384,11 @@ async def reflect_on_profile(
|
||||
model = os.getenv("ANTHROPIC_MODEL", "claude-sonnet-4-6")
|
||||
try:
|
||||
coach = ReflectionCoach(api_key=api_key, model=model)
|
||||
result = await coach.reflect(_profile_fields(profile) | {"triad": profile.triad}, coach_history)
|
||||
result = await coach.reflect(
|
||||
_profile_fields(profile) | {"triad": profile.triad},
|
||||
coach_history,
|
||||
focus=payload.focus,
|
||||
)
|
||||
except ReflectionError as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
|
||||
@@ -459,6 +464,44 @@ async def get_profile_revisions(
|
||||
return out
|
||||
|
||||
|
||||
@router.get(
|
||||
"/profile/me/goal-history", response_model=schemas.GoalHistoryOut
|
||||
)
|
||||
async def get_goal_history(
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""How the person's near/long-term goals evolved over time, derived from
|
||||
the profile revision snapshots (Phase 5)."""
|
||||
profile = await _latest_profile(db, user.id)
|
||||
if profile is None:
|
||||
raise HTTPException(status_code=404, detail="No profile for this user")
|
||||
rev_stmt = (
|
||||
select(ProfileRevision)
|
||||
.where(ProfileRevision.profile_id == profile.id)
|
||||
.order_by(ProfileRevision.created_at)
|
||||
)
|
||||
rows = (await db.execute(rev_stmt)).scalars().all()
|
||||
revisions = []
|
||||
for r in rows:
|
||||
try:
|
||||
fields = json.loads(r.fields_json)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
fields = {}
|
||||
revisions.append(
|
||||
{"fields": fields, "source": r.source, "created_at": r.created_at}
|
||||
)
|
||||
timelines = goal_history(revisions)
|
||||
return schemas.GoalHistoryOut(
|
||||
short_term_goals=[
|
||||
schemas.GoalHistoryEntry(**e) for e in timelines["short_term_goals"]
|
||||
],
|
||||
long_term_goals=[
|
||||
schemas.GoalHistoryEntry(**e) for e in timelines["long_term_goals"]
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/conversation/{conversation_id}",
|
||||
response_model=schemas.ConversationResponse,
|
||||
|
||||
@@ -9,6 +9,7 @@ up to feed the coaching reminder engine and the goal dashboard.
|
||||
All routes are user-scoped via the dual-auth dependency; the tracker calls as
|
||||
the user (forwarded session/JWT) or, service-to-service, with ``X-API-Key``.
|
||||
"""
|
||||
import os
|
||||
import uuid
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
@@ -25,6 +26,7 @@ from app.services.foundations import (
|
||||
FOUNDATIONS,
|
||||
rollup,
|
||||
)
|
||||
from app.services.tagging import FoundationTagger, TaggingError
|
||||
|
||||
router = APIRouter(prefix="/discovery/integration", tags=["integration"])
|
||||
|
||||
@@ -141,6 +143,44 @@ async def work_patterns_for(
|
||||
return rollup(entries)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/suggest-foundation", response_model=schemas.SuggestFoundationOut
|
||||
)
|
||||
async def suggest_foundation(
|
||||
payload: schemas.SuggestFoundationRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
user: User = Depends(get_current_user),
|
||||
):
|
||||
"""Phase 5 smart tagging: suggest which foundation a task builds toward.
|
||||
The person confirms by posting the task mapping. Suggestion only — nothing
|
||||
is stored here."""
|
||||
label = payload.task_label.strip()
|
||||
if not label:
|
||||
raise HTTPException(status_code=400, detail="task_label is required")
|
||||
profile = await _latest_profile(db, user.id)
|
||||
if profile is None:
|
||||
raise HTTPException(status_code=404, detail="No profile for this user")
|
||||
|
||||
foundations_text = {
|
||||
key: getattr(profile, field, None)
|
||||
for key, field in FOUNDATION_TO_FIELD.items()
|
||||
}
|
||||
api_key = os.getenv("ANTHROPIC_API_KEY")
|
||||
model = os.getenv("ANTHROPIC_MODEL", "claude-sonnet-4-6")
|
||||
try:
|
||||
tagger = FoundationTagger(api_key=api_key, model=model)
|
||||
result = await tagger.suggest(label, foundations_text)
|
||||
except TaggingError as exc:
|
||||
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
||||
|
||||
return schemas.SuggestFoundationOut(
|
||||
foundation=result["foundation"],
|
||||
label=FOUNDATIONS[result["foundation"]],
|
||||
rationale=result["rationale"],
|
||||
confidence=result["confidence"],
|
||||
)
|
||||
|
||||
|
||||
@router.get("/work-patterns", response_model=schemas.WorkPatternsOut)
|
||||
async def get_work_patterns(
|
||||
days: int = Query(30, ge=1, le=365),
|
||||
|
||||
Reference in New Issue
Block a user