Compare commits
4 Commits
kimi/issue
...
kimi/issue
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a9ff78b1c | ||
| 6f66670396 | |||
| 4cdd82818b | |||
| 99ad672e4d |
@@ -1,4 +1,4 @@
|
||||
from datetime import date, datetime
|
||||
from datetime import UTC, date, datetime
|
||||
from enum import StrEnum
|
||||
|
||||
from sqlalchemy import JSON, Boolean, Column, Date, DateTime, Index, Integer, String
|
||||
@@ -40,8 +40,13 @@ class Task(Base):
|
||||
deferred_at = Column(DateTime, nullable=True)
|
||||
|
||||
# Timestamps
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow, nullable=False)
|
||||
created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)
|
||||
updated_at = Column(
|
||||
DateTime,
|
||||
default=lambda: datetime.now(UTC),
|
||||
onupdate=lambda: datetime.now(UTC),
|
||||
nullable=False,
|
||||
)
|
||||
|
||||
__table_args__ = (Index("ix_task_state_order", "state", "sort_order"),)
|
||||
|
||||
@@ -59,4 +64,4 @@ class JournalEntry(Base):
|
||||
gratitude = Column(String(500), nullable=True)
|
||||
energy_level = Column(Integer, nullable=True) # User-reported, 1-10
|
||||
|
||||
created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
|
||||
created_at = Column(DateTime, default=lambda: datetime.now(UTC), nullable=False)
|
||||
|
||||
@@ -5,7 +5,7 @@ import sqlite3
|
||||
import uuid
|
||||
from collections.abc import Generator
|
||||
from contextlib import closing, contextmanager
|
||||
from datetime import datetime
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Form, HTTPException, Request
|
||||
@@ -219,7 +219,7 @@ async def create_task_form(
|
||||
raise HTTPException(status_code=400, detail="Task title cannot be empty")
|
||||
|
||||
task_id = str(uuid.uuid4())
|
||||
now = datetime.utcnow().isoformat()
|
||||
now = datetime.now(UTC).isoformat()
|
||||
priority = priority if priority in VALID_PRIORITIES else "normal"
|
||||
|
||||
with _get_db() as db:
|
||||
@@ -287,7 +287,7 @@ async def modify_task(
|
||||
async def _set_status(request: Request, task_id: str, new_status: str):
|
||||
"""Helper to update status and return refreshed task card."""
|
||||
completed_at = (
|
||||
datetime.utcnow().isoformat() if new_status in ("completed", "vetoed", "failed") else None
|
||||
datetime.now(UTC).isoformat() if new_status in ("completed", "vetoed", "failed") else None
|
||||
)
|
||||
with _get_db() as db:
|
||||
db.execute(
|
||||
@@ -316,7 +316,7 @@ async def api_create_task(request: Request):
|
||||
raise HTTPException(422, "title is required")
|
||||
|
||||
task_id = str(uuid.uuid4())
|
||||
now = datetime.utcnow().isoformat()
|
||||
now = datetime.now(UTC).isoformat()
|
||||
priority = body.get("priority", "normal")
|
||||
if priority not in VALID_PRIORITIES:
|
||||
priority = "normal"
|
||||
@@ -358,7 +358,7 @@ async def api_update_status(task_id: str, request: Request):
|
||||
raise HTTPException(422, f"Invalid status. Must be one of: {VALID_STATUSES}")
|
||||
|
||||
completed_at = (
|
||||
datetime.utcnow().isoformat() if new_status in ("completed", "vetoed", "failed") else None
|
||||
datetime.now(UTC).isoformat() if new_status in ("completed", "vetoed", "failed") else None
|
||||
)
|
||||
with _get_db() as db:
|
||||
db.execute(
|
||||
|
||||
@@ -5,7 +5,7 @@ import sqlite3
|
||||
import uuid
|
||||
from collections.abc import Generator
|
||||
from contextlib import closing, contextmanager
|
||||
from datetime import datetime
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, Form, HTTPException, Request
|
||||
@@ -144,7 +144,7 @@ async def submit_work_order(
|
||||
related_files: str = Form(""),
|
||||
):
|
||||
wo_id = str(uuid.uuid4())
|
||||
now = datetime.utcnow().isoformat()
|
||||
now = datetime.now(UTC).isoformat()
|
||||
priority = priority if priority in PRIORITIES else "medium"
|
||||
category = category if category in CATEGORIES else "suggestion"
|
||||
|
||||
@@ -211,7 +211,7 @@ async def active_partial(request: Request):
|
||||
|
||||
async def _update_status(request: Request, wo_id: str, new_status: str, **extra):
|
||||
completed_at = (
|
||||
datetime.utcnow().isoformat() if new_status in ("completed", "rejected") else None
|
||||
datetime.now(UTC).isoformat() if new_status in ("completed", "rejected") else None
|
||||
)
|
||||
with _get_db() as db:
|
||||
sets = ["status=?", "completed_at=COALESCE(?, completed_at)"]
|
||||
|
||||
@@ -30,7 +30,7 @@ import shutil
|
||||
import sqlite3
|
||||
import uuid
|
||||
from contextlib import closing
|
||||
from datetime import datetime
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
|
||||
import httpx
|
||||
@@ -196,7 +196,7 @@ def _bridge_to_work_order(title: str, body: str, category: str) -> None:
|
||||
body,
|
||||
category,
|
||||
"timmy-thinking",
|
||||
datetime.utcnow().isoformat(),
|
||||
datetime.now(UTC).isoformat(),
|
||||
),
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
@@ -392,31 +392,26 @@ def _build_insights(
|
||||
return insights or ["Conversations look healthy. Keep up the good work."]
|
||||
|
||||
|
||||
def self_reflect(limit: int = 30) -> str:
|
||||
"""Review recent conversations and reflect on Timmy's own behavior.
|
||||
def _format_recurring_topics(repeated: list[tuple[str, int]]) -> list[str]:
|
||||
"""Format the recurring-topics section of the reflection report."""
|
||||
if repeated:
|
||||
lines = ["### Recurring Topics"]
|
||||
for word, count in repeated:
|
||||
lines.append(f'- "{word}" ({count} mentions)')
|
||||
lines.append("")
|
||||
return lines
|
||||
return ["### Recurring Topics\nNo strong patterns detected.\n"]
|
||||
|
||||
Scans past session entries for patterns: low-confidence responses,
|
||||
errors, repeated topics, and conversation quality signals. Returns
|
||||
a structured reflection that Timmy can use to improve.
|
||||
|
||||
Args:
|
||||
limit: How many recent entries to review (default 30).
|
||||
|
||||
Returns:
|
||||
A formatted self-reflection report.
|
||||
"""
|
||||
sl = get_session_logger()
|
||||
sl.flush()
|
||||
entries = sl.get_recent_entries(limit=limit)
|
||||
|
||||
if not entries:
|
||||
return "No conversation history to reflect on yet."
|
||||
|
||||
_messages, errors, timmy_msgs, user_msgs = _categorize_entries(entries)
|
||||
low_conf = _find_low_confidence(timmy_msgs)
|
||||
repeated = _find_repeated_topics(user_msgs)
|
||||
|
||||
# Build reflection report
|
||||
def _build_reflection_report(
|
||||
entries: list[dict],
|
||||
errors: list[dict],
|
||||
timmy_msgs: list[dict],
|
||||
user_msgs: list[dict],
|
||||
low_conf: list[dict],
|
||||
repeated: list[tuple[str, int]],
|
||||
) -> str:
|
||||
"""Assemble the full self-reflection report from analysed data."""
|
||||
sections: list[str] = ["## Self-Reflection Report\n"]
|
||||
sections.append(
|
||||
f"Reviewed {len(entries)} recent entries: "
|
||||
@@ -446,16 +441,37 @@ def self_reflect(limit: int = 30) -> str:
|
||||
)
|
||||
)
|
||||
|
||||
if repeated:
|
||||
sections.append("### Recurring Topics")
|
||||
for word, count in repeated:
|
||||
sections.append(f'- "{word}" ({count} mentions)')
|
||||
sections.append("")
|
||||
else:
|
||||
sections.append("### Recurring Topics\nNo strong patterns detected.\n")
|
||||
sections.extend(_format_recurring_topics(repeated))
|
||||
|
||||
sections.append("### Insights")
|
||||
for insight in _build_insights(low_conf, errors, repeated):
|
||||
sections.append(f"- {insight}")
|
||||
|
||||
return "\n".join(sections)
|
||||
|
||||
|
||||
def self_reflect(limit: int = 30) -> str:
|
||||
"""Review recent conversations and reflect on Timmy's own behavior.
|
||||
|
||||
Scans past session entries for patterns: low-confidence responses,
|
||||
errors, repeated topics, and conversation quality signals. Returns
|
||||
a structured reflection that Timmy can use to improve.
|
||||
|
||||
Args:
|
||||
limit: How many recent entries to review (default 30).
|
||||
|
||||
Returns:
|
||||
A formatted self-reflection report.
|
||||
"""
|
||||
sl = get_session_logger()
|
||||
sl.flush()
|
||||
entries = sl.get_recent_entries(limit=limit)
|
||||
|
||||
if not entries:
|
||||
return "No conversation history to reflect on yet."
|
||||
|
||||
_messages, errors, timmy_msgs, user_msgs = _categorize_entries(entries)
|
||||
low_conf = _find_low_confidence(timmy_msgs)
|
||||
repeated = _find_repeated_topics(user_msgs)
|
||||
|
||||
return _build_reflection_report(entries, errors, timmy_msgs, user_msgs, low_conf, repeated)
|
||||
|
||||
@@ -89,45 +89,31 @@ def list_swarm_agents() -> dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
def delegate_to_kimi(task: str, working_directory: str = "") -> dict[str, Any]:
|
||||
"""Delegate a coding task to Kimi, the external coding agent.
|
||||
|
||||
Kimi has 262K context and is optimized for code tasks: writing,
|
||||
debugging, refactoring, test writing. Timmy thinks and plans,
|
||||
Kimi executes bulk code changes.
|
||||
|
||||
Args:
|
||||
task: Clear, specific coding task description. Include file paths
|
||||
and expected behavior. Good: "Fix the bug in src/timmy/session.py
|
||||
where sessions don't persist." Bad: "Fix all bugs."
|
||||
working_directory: Directory for Kimi to work in. Defaults to repo root.
|
||||
|
||||
Returns:
|
||||
Dict with success status and Kimi's output or error.
|
||||
"""
|
||||
def _find_kimi_cli() -> str | None:
|
||||
"""Return the path to the kimi CLI binary, or None if not installed."""
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
return shutil.which("kimi")
|
||||
|
||||
|
||||
def _resolve_workdir(working_directory: str) -> str | dict[str, Any]:
|
||||
"""Return a validated working directory path, or an error dict."""
|
||||
from pathlib import Path
|
||||
|
||||
from config import settings
|
||||
|
||||
kimi_path = shutil.which("kimi")
|
||||
if not kimi_path:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "kimi CLI not found on PATH. Install with: pip install kimi-cli",
|
||||
}
|
||||
|
||||
workdir = working_directory or settings.repo_root
|
||||
if not Path(workdir).is_dir():
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"Working directory does not exist: {workdir}",
|
||||
}
|
||||
return workdir
|
||||
|
||||
cmd = [kimi_path, "--print", "-p", task]
|
||||
|
||||
logger.info("Delegating to Kimi: %s (cwd=%s)", task[:80], workdir)
|
||||
def _run_kimi(cmd: list[str], workdir: str) -> dict[str, Any]:
|
||||
"""Execute the kimi subprocess and return a result dict."""
|
||||
import subprocess
|
||||
|
||||
try:
|
||||
result = subprocess.run(
|
||||
@@ -157,3 +143,34 @@ def delegate_to_kimi(task: str, working_directory: str = "") -> dict[str, Any]:
|
||||
"success": False,
|
||||
"error": f"Failed to run Kimi: {exc}",
|
||||
}
|
||||
|
||||
|
||||
def delegate_to_kimi(task: str, working_directory: str = "") -> dict[str, Any]:
|
||||
"""Delegate a coding task to Kimi, the external coding agent.
|
||||
|
||||
Kimi has 262K context and is optimized for code tasks: writing,
|
||||
debugging, refactoring, test writing. Timmy thinks and plans,
|
||||
Kimi executes bulk code changes.
|
||||
|
||||
Args:
|
||||
task: Clear, specific coding task description. Include file paths
|
||||
and expected behavior. Good: "Fix the bug in src/timmy/session.py
|
||||
where sessions don't persist." Bad: "Fix all bugs."
|
||||
working_directory: Directory for Kimi to work in. Defaults to repo root.
|
||||
|
||||
Returns:
|
||||
Dict with success status and Kimi's output or error.
|
||||
"""
|
||||
kimi_path = _find_kimi_cli()
|
||||
if not kimi_path:
|
||||
return {
|
||||
"success": False,
|
||||
"error": "kimi CLI not found on PATH. Install with: pip install kimi-cli",
|
||||
}
|
||||
|
||||
workdir = _resolve_workdir(working_directory)
|
||||
if isinstance(workdir, dict):
|
||||
return workdir
|
||||
|
||||
logger.info("Delegating to Kimi: %s (cwd=%s)", task[:80], workdir)
|
||||
return _run_kimi([kimi_path, "--print", "-p", task], workdir)
|
||||
|
||||
@@ -86,6 +86,40 @@ def _pip_snapshot(mood: str, confidence: float) -> dict:
|
||||
return pip_familiar.snapshot().to_dict()
|
||||
|
||||
|
||||
def _resolve_mood(state) -> str:
|
||||
"""Map cognitive mood/engagement to a presence mood string."""
|
||||
if state.engagement == "idle" and state.mood == "settled":
|
||||
return "calm"
|
||||
return _MOOD_MAP.get(state.mood, "calm")
|
||||
|
||||
|
||||
def _resolve_confidence(state) -> float:
|
||||
"""Compute normalised confidence from cognitive tracker state."""
|
||||
if state._confidence_count > 0:
|
||||
raw = state._confidence_sum / state._confidence_count
|
||||
else:
|
||||
raw = 0.7
|
||||
return round(max(0.0, min(1.0, raw)), 2)
|
||||
|
||||
|
||||
def _build_active_threads(state) -> list[dict]:
|
||||
"""Convert active commitments into presence thread dicts."""
|
||||
return [
|
||||
{"type": "thinking", "ref": c[:80], "status": "active"}
|
||||
for c in state.active_commitments[:10]
|
||||
]
|
||||
|
||||
|
||||
def _build_environment() -> dict:
|
||||
"""Return the environment section using local wall-clock time."""
|
||||
local_now = datetime.now()
|
||||
return {
|
||||
"time_of_day": _time_of_day(local_now.hour),
|
||||
"local_time": local_now.strftime("%-I:%M %p"),
|
||||
"day_of_week": local_now.strftime("%A"),
|
||||
}
|
||||
|
||||
|
||||
def get_state_dict() -> dict:
|
||||
"""Build presence state dict from current cognitive state.
|
||||
|
||||
@@ -98,37 +132,19 @@ def get_state_dict() -> dict:
|
||||
state = cognitive_tracker.get_state()
|
||||
now = datetime.now(UTC)
|
||||
|
||||
# Map cognitive mood to presence mood
|
||||
mood = _MOOD_MAP.get(state.mood, "calm")
|
||||
if state.engagement == "idle" and state.mood == "settled":
|
||||
mood = "calm"
|
||||
|
||||
# Confidence from cognitive tracker
|
||||
if state._confidence_count > 0:
|
||||
confidence = state._confidence_sum / state._confidence_count
|
||||
else:
|
||||
confidence = 0.7
|
||||
|
||||
# Build active threads from commitments
|
||||
threads = []
|
||||
for commitment in state.active_commitments[:10]:
|
||||
threads.append({"type": "thinking", "ref": commitment[:80], "status": "active"})
|
||||
|
||||
# Activity
|
||||
mood = _resolve_mood(state)
|
||||
confidence = _resolve_confidence(state)
|
||||
activity = _ACTIVITY_MAP.get(state.engagement, "idle")
|
||||
|
||||
# Environment
|
||||
local_now = datetime.now()
|
||||
|
||||
return {
|
||||
"version": 1,
|
||||
"liveness": now.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
||||
"current_focus": state.focus_topic or "",
|
||||
"active_threads": threads,
|
||||
"active_threads": _build_active_threads(state),
|
||||
"recent_events": [],
|
||||
"concerns": [],
|
||||
"mood": mood,
|
||||
"confidence": round(max(0.0, min(1.0, confidence)), 2),
|
||||
"confidence": confidence,
|
||||
"energy": round(_current_energy(), 2),
|
||||
"identity": {
|
||||
"name": "Timmy",
|
||||
@@ -143,11 +159,7 @@ def get_state_dict() -> dict:
|
||||
"visitor_present": False,
|
||||
"conversation_turns": state.conversation_depth,
|
||||
},
|
||||
"environment": {
|
||||
"time_of_day": _time_of_day(local_now.hour),
|
||||
"local_time": local_now.strftime("%-I:%M %p"),
|
||||
"day_of_week": local_now.strftime("%A"),
|
||||
},
|
||||
"environment": _build_environment(),
|
||||
"familiar": _pip_snapshot(mood, confidence),
|
||||
"meta": {
|
||||
"schema_version": 1,
|
||||
|
||||
Reference in New Issue
Block a user