mirror of
https://github.com/x1xhlol/system-prompts-and-models-of-ai-tools.git
synced 2026-06-18 15:29:36 +00:00
knowledge_brain.py, memory_engine.py, session_continuity.py used parents[4] to find memory/ dir. In Docker (/app/app/services/file.py) there are only 4 parents total, causing IndexError: 4. Fix: walk up parents dynamically until memory/ dir is found. https://claude.ai/code/session_01W1rJthWDkasijTdXCfxVHs
235 lines
11 KiB
Python
235 lines
11 KiB
Python
"""
|
|
Session Continuity — Dealix AI Session State Management
|
|
Maintains context across AI agent sessions for seamless handoff.
|
|
"""
|
|
import json, logging, uuid
|
|
from datetime import datetime, timedelta, timezone
|
|
from pathlib import Path
|
|
from typing import Any, Optional
|
|
from pydantic import BaseModel, Field
|
|
|
|
logger = logging.getLogger(__name__)
|
|
def _find_sessions():
|
|
for p in Path(__file__).resolve().parents:
|
|
if (p / "memory").is_dir():
|
|
return p / "memory" / "_sessions"
|
|
return Path(__file__).resolve().parent.parent / "memory" / "_sessions"
|
|
SESSIONS_DIR = _find_sessions()
|
|
SESSIONS_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
class Decision(BaseModel):
|
|
"""قرار مسجّل"""
|
|
decision: str; context: str; decision_ar: str = ""; made_by: str = ""
|
|
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
|
|
class Failure(BaseModel):
|
|
"""فشل مسجّل"""
|
|
description: str; context: str; description_ar: str = ""; resolution: str = ""
|
|
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
|
|
class Win(BaseModel):
|
|
"""نجاح مسجّل"""
|
|
description: str; context: str; description_ar: str = ""
|
|
timestamp: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
|
|
class FollowUp(BaseModel):
|
|
"""مهمة متابعة معلّقة"""
|
|
task: str; task_ar: str = ""; due_date: Optional[datetime] = None
|
|
completed: bool = False; assigned_to: str = ""
|
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
|
|
class SessionState(BaseModel):
|
|
"""حالة الجلسة الكاملة"""
|
|
session_id: str = Field(default_factory=lambda: str(uuid.uuid4()))
|
|
project: str = "dealix"; active_workstreams: list[str] = []
|
|
last_decisions: list[Decision] = []; open_questions: list[str] = []
|
|
recent_failures: list[Failure] = []; recent_wins: list[Win] = []
|
|
pending_followups: list[FollowUp] = []
|
|
context_summary: str = ""; context_summary_ar: str = ""
|
|
created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc))
|
|
tags: list[str] = []; tenant_id: str = ""
|
|
|
|
|
|
def _dt_hook(obj: Any) -> Any:
|
|
"""Convert datetime strings in nested dicts."""
|
|
if isinstance(obj, dict):
|
|
for k in ("timestamp", "created_at", "updated_at", "due_date"):
|
|
if k in obj and isinstance(obj[k], str) and obj[k]:
|
|
obj[k] = datetime.fromisoformat(obj[k])
|
|
return obj
|
|
return obj
|
|
|
|
|
|
class SessionContinuity:
|
|
"""الحفاظ على السياق عبر جلسات الذكاء الاصطناعي"""
|
|
|
|
def __init__(self, sessions_dir: Path = None):
|
|
self.dir = sessions_dir or SESSIONS_DIR
|
|
self.dir.mkdir(parents=True, exist_ok=True)
|
|
self._current: Optional[SessionState] = None
|
|
|
|
def _path(self, sid: str) -> Path: return self.dir / f"{sid}.json"
|
|
|
|
def _save_json(self, state: SessionState) -> None:
|
|
data = state.model_dump(mode="json")
|
|
# Ensure all datetimes are ISO strings
|
|
for key in ("created_at", "updated_at"):
|
|
if isinstance(data.get(key), datetime): data[key] = data[key].isoformat()
|
|
for lst in ("last_decisions", "recent_failures", "recent_wins", "pending_followups"):
|
|
for item in data.get(lst, []):
|
|
for dk in ("timestamp", "created_at", "updated_at", "due_date"):
|
|
if dk in item and isinstance(item[dk], datetime): item[dk] = item[dk].isoformat()
|
|
self._path(state.session_id).write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
|
|
def _load_json(self, path: Path) -> SessionState:
|
|
data = json.loads(path.read_text(encoding="utf-8"))
|
|
for key in ("created_at", "updated_at"): _dt_hook(data) if key in data else None
|
|
for lst in ("last_decisions", "recent_failures", "recent_wins", "pending_followups"):
|
|
for item in data.get(lst, []): _dt_hook(item)
|
|
# Parse top-level dates
|
|
for k in ("created_at", "updated_at"):
|
|
if isinstance(data.get(k), str): data[k] = datetime.fromisoformat(data[k])
|
|
return SessionState(**data)
|
|
|
|
async def save_state(self, state: SessionState) -> str:
|
|
"""حفظ حالة الجلسة."""
|
|
state.updated_at = datetime.now(timezone.utc)
|
|
self._save_json(state); self._current = state
|
|
logger.info("حفظ جلسة: %s", state.session_id); return state.session_id
|
|
|
|
async def restore_state(self, session_id: str = None) -> SessionState:
|
|
"""استعادة حالة الجلسة — الأحدث إذا لم يُحدد معرف."""
|
|
if session_id:
|
|
p = self._path(session_id)
|
|
if p.exists():
|
|
s = self._load_json(p); self._current = s; return s
|
|
return SessionState(session_id=session_id)
|
|
# Find latest
|
|
latest = max(self.dir.glob("*.json"), key=lambda f: f.stat().st_mtime, default=None)
|
|
if latest:
|
|
s = self._load_json(latest); self._current = s; return s
|
|
s = SessionState(); await self.save_state(s); return s
|
|
|
|
async def get_restore_prompt(self) -> str:
|
|
"""توليد نص ملخّص للحالة الحالية لتغذية جلسة جديدة."""
|
|
s = self._current or await self.restore_state()
|
|
lines = [f"# Session Restore — استعادة الجلسة",
|
|
f"**Project**: {s.project} | **Session**: {s.session_id}",
|
|
f"**Updated**: {s.updated_at.isoformat()}", ""]
|
|
if s.context_summary:
|
|
lines += ["## Context (السياق)", s.context_summary]
|
|
if s.context_summary_ar: lines.append(s.context_summary_ar)
|
|
lines.append("")
|
|
if s.active_workstreams:
|
|
lines += ["## Workstreams (مسارات العمل)"] + [f"- {w}" for w in s.active_workstreams] + [""]
|
|
if s.last_decisions:
|
|
lines.append("## Decisions (القرارات)")
|
|
for d in s.last_decisions[-5:]:
|
|
lines.append(f"- [{d.timestamp:%Y-%m-%d %H:%M}] {d.decision}")
|
|
if d.decision_ar: lines.append(f" {d.decision_ar}")
|
|
lines.append("")
|
|
if s.open_questions:
|
|
lines += ["## Questions (أسئلة)"] + [f"- {q}" for q in s.open_questions] + [""]
|
|
if s.recent_failures:
|
|
lines.append("## Failures (إخفاقات)")
|
|
for f in s.recent_failures[-3:]:
|
|
lines.append(f"- {f.description}")
|
|
if f.resolution: lines.append(f" Fix: {f.resolution}")
|
|
lines.append("")
|
|
if s.recent_wins:
|
|
lines += ["## Wins (نجاحات)"] + [f"- {w.description}" for w in s.recent_wins[-3:]] + [""]
|
|
pending = [fu for fu in s.pending_followups if not fu.completed]
|
|
if pending:
|
|
lines.append("## Follow-ups (متابعات)")
|
|
for fu in pending:
|
|
due = f" (due: {fu.due_date:%Y-%m-%d})" if fu.due_date else ""
|
|
lines.append(f"- {fu.task}{due}")
|
|
lines.append("")
|
|
lines += ["---", "Continue from this state. Prioritize pending follow-ups.",
|
|
"استمر من هذه الحالة. أعطِ الأولوية للمتابعات المعلّقة."]
|
|
return "\n".join(lines)
|
|
|
|
async def _ensure_current(self) -> SessionState:
|
|
if not self._current: self._current = await self.restore_state()
|
|
return self._current
|
|
|
|
async def add_decision(self, decision: str, context: str, decision_ar: str = "", made_by: str = "") -> None:
|
|
"""تسجيل قرار."""
|
|
s = await self._ensure_current()
|
|
s.last_decisions.append(Decision(decision=decision, context=context, decision_ar=decision_ar, made_by=made_by))
|
|
if len(s.last_decisions) > 20: s.last_decisions = s.last_decisions[-20:]
|
|
await self.save_state(s)
|
|
|
|
async def add_failure(self, description: str, context: str, description_ar: str = "", resolution: str = "") -> None:
|
|
"""تسجيل فشل."""
|
|
s = await self._ensure_current()
|
|
s.recent_failures.append(Failure(description=description, context=context, description_ar=description_ar, resolution=resolution))
|
|
if len(s.recent_failures) > 10: s.recent_failures = s.recent_failures[-10:]
|
|
await self.save_state(s)
|
|
|
|
async def add_win(self, description: str, context: str, description_ar: str = "") -> None:
|
|
"""تسجيل نجاح."""
|
|
s = await self._ensure_current()
|
|
s.recent_wins.append(Win(description=description, context=context, description_ar=description_ar))
|
|
if len(s.recent_wins) > 10: s.recent_wins = s.recent_wins[-10:]
|
|
await self.save_state(s)
|
|
|
|
async def add_followup(self, task: str, due_date: datetime = None, task_ar: str = "", assigned_to: str = "") -> None:
|
|
"""إضافة مهمة متابعة."""
|
|
s = await self._ensure_current()
|
|
s.pending_followups.append(FollowUp(task=task, task_ar=task_ar, due_date=due_date, assigned_to=assigned_to))
|
|
await self.save_state(s)
|
|
|
|
async def complete_followup(self, task_substring: str) -> bool:
|
|
"""تعليم متابعة كمكتملة."""
|
|
s = await self._ensure_current()
|
|
tl = task_substring.lower()
|
|
for fu in s.pending_followups:
|
|
if tl in fu.task.lower() and not fu.completed:
|
|
fu.completed = True; await self.save_state(s); return True
|
|
return False
|
|
|
|
async def set_workstreams(self, workstreams: list[str]) -> None:
|
|
s = await self._ensure_current(); s.active_workstreams = workstreams; await self.save_state(s)
|
|
|
|
async def set_context(self, summary: str, summary_ar: str = "") -> None:
|
|
s = await self._ensure_current()
|
|
s.context_summary = summary; s.context_summary_ar = summary_ar; await self.save_state(s)
|
|
|
|
async def add_question(self, question: str) -> None:
|
|
s = await self._ensure_current()
|
|
if question not in s.open_questions:
|
|
s.open_questions.append(question)
|
|
if len(s.open_questions) > 15: s.open_questions = s.open_questions[-15:]
|
|
await self.save_state(s)
|
|
|
|
async def cleanup_old_sessions(self, days: int = 30) -> int:
|
|
"""حذف جلسات أقدم من N يوم."""
|
|
cutoff, removed = datetime.now(timezone.utc) - timedelta(days=days), 0
|
|
for f in self.dir.glob("*.json"):
|
|
try:
|
|
d = json.loads(f.read_text(encoding="utf-8"))
|
|
u = d.get("updated_at", "")
|
|
if isinstance(u, str) and u and datetime.fromisoformat(u) < cutoff:
|
|
f.unlink(); removed += 1
|
|
except Exception: pass
|
|
logger.info("حذف %d جلسة قديمة", removed); return removed
|
|
|
|
async def list_sessions(self, limit: int = 20) -> list[dict[str, Any]]:
|
|
"""عرض الجلسات الأخيرة."""
|
|
sessions = []
|
|
for f in sorted(self.dir.glob("*.json"), key=lambda p: p.stat().st_mtime, reverse=True)[:limit]:
|
|
try:
|
|
d = json.loads(f.read_text(encoding="utf-8"))
|
|
sessions.append({"session_id": d.get("session_id", f.stem), "project": d.get("project", ""),
|
|
"updated_at": d.get("updated_at", ""), "workstreams": d.get("active_workstreams", []),
|
|
"decisions": len(d.get("last_decisions", [])),
|
|
"pending": sum(1 for fu in d.get("pending_followups", []) if not fu.get("completed"))})
|
|
except Exception: continue
|
|
return sessions
|
|
|
|
|
|
session_continuity = SessionContinuity()
|