c889a57b6b
Test Suites / Build CI Environment (push) Has been cancelled
Test Suites / Basic Tests (push) Has been cancelled
Test Suites / End-to-End Tests (push) Has been cancelled
Test Suites / CLI Tests (push) Has been cancelled
Test Suites / Slow End-to-End Tests (push) Has been cancelled
Test Suites / Graph Database Tests (push) Has been cancelled
Test Suites / Vector DB Tests (push) Has been cancelled
Test Suites / Temporal Graph Test (push) Has been cancelled
Test Suites / Search Test on Different DBs (push) Has been cancelled
Test Suites / Example Tests (push) Has been cancelled
Test Suites / Notebook Tests (push) Has been cancelled
Test Suites / OS and Python Tests Ubuntu (push) Has been cancelled
Test Suites / OS and Python Tests Extended (push) Has been cancelled
Test Suites / LLM Test Suite (push) Has been cancelled
Test Suites / S3 File Storage Test (push) Has been cancelled
Test Suites / Run Integration Tests (push) Has been cancelled
Test Suites / MCP Tests (push) Has been cancelled
Test Suites / Docker Compose Test (push) Has been cancelled
Test Suites / Docker CI test (push) Has been cancelled
Test Suites / Relational DB Migration Tests (push) Has been cancelled
Test Suites / Distributed Cognee Test (push) Has been cancelled
Test Suites / DB Examples Tests (push) Has been cancelled
Test Suites / Test Completion Status (push) Has been cancelled
Test Suites / Claude Code Review (push) Has been cancelled
Test Suites / basic checks (push) Has been cancelled
build | Build and Push Cognee MCP Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
build | Build and Push Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.11) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.12) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (kuzu, kuzu) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (neo4j, neo4j) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Examples (push) Has been cancelled
Weighted Edges Tests / Code Quality for Weighted Edges (push) Has been cancelled
498 lines
17 KiB
Python
498 lines
17 KiB
Python
from enum import Enum
|
|
from typing import Annotated, List, Literal, Optional
|
|
|
|
from pydantic import BaseModel, Field, field_validator, model_validator
|
|
|
|
VALID_RATINGS = {"helpful", "harmful"}
|
|
MAX_CONTEXT_CONTENT_CHARS = 280
|
|
MIN_CANDIDATE_CONFIDENCE = 0.75
|
|
|
|
|
|
class ContextSection(str, Enum):
|
|
"""Sections under which active session-context guidance is grouped."""
|
|
|
|
GOALS = "goals"
|
|
RULES = "rules"
|
|
PREFERENCES = "preferences"
|
|
LESSONS_LEARNED = "lessons_learned"
|
|
|
|
|
|
VALID_SECTIONS = {section.value for section in ContextSection}
|
|
|
|
|
|
class ContextProfile(str, Enum):
|
|
"""Which kind of consumer a session-context lesson is for."""
|
|
|
|
QA = "qa"
|
|
AGENT = "agent"
|
|
|
|
|
|
VALID_PROFILES = {profile.value for profile in ContextProfile}
|
|
|
|
|
|
class AgentContextSection(str, Enum):
|
|
"""Sections for agent/tool-loop session-context guidance."""
|
|
|
|
TOOL_RULES = "tool_rules"
|
|
WORKFLOW_STATE = "workflow_state"
|
|
SUCCESS_PATTERNS = "success_patterns"
|
|
FAILURE_LESSONS = "failure_lessons"
|
|
ENVIRONMENT_FACTS = "environment_facts"
|
|
|
|
|
|
AGENT_VALID_SECTIONS = {section.value for section in AgentContextSection}
|
|
|
|
SECTIONS_BY_PROFILE = {
|
|
ContextProfile.QA.value: VALID_SECTIONS,
|
|
ContextProfile.AGENT.value: AGENT_VALID_SECTIONS,
|
|
}
|
|
|
|
|
|
def valid_sections_for(profile: str) -> set:
|
|
"""Allowed section names for a context profile; empty set for an unknown profile."""
|
|
return SECTIONS_BY_PROFILE.get(profile, set())
|
|
|
|
|
|
def normalize_content(text: str) -> str:
|
|
"""Deterministic exact-match key: lowercased, whitespace-collapsed, stripped."""
|
|
if not isinstance(text, str):
|
|
raise ValueError("text must be a string")
|
|
return " ".join(text.strip().lower().split())
|
|
|
|
|
|
class ServedContextRating(BaseModel):
|
|
"""A single rating of a session-context entry that was served to the previous answer."""
|
|
|
|
entry_id: str
|
|
rating: str
|
|
|
|
@field_validator("entry_id")
|
|
@classmethod
|
|
def entry_id_non_empty(cls, v: str) -> str:
|
|
if not isinstance(v, str):
|
|
raise ValueError("entry_id must be a string")
|
|
stripped = v.strip()
|
|
if not stripped:
|
|
raise ValueError("entry_id must be a non-empty string")
|
|
return stripped
|
|
|
|
@field_validator("rating")
|
|
@classmethod
|
|
def rating_valid(cls, v: str) -> str:
|
|
if not isinstance(v, str):
|
|
raise ValueError("rating must be a string")
|
|
normalized = v.strip().lower()
|
|
if normalized not in VALID_RATINGS:
|
|
raise ValueError(f"rating must be one of {sorted(VALID_RATINGS)}")
|
|
return normalized
|
|
|
|
|
|
class CandidateContextUpdate(BaseModel):
|
|
"""A proposed new active session-context guidance entry emitted by feedback detection."""
|
|
|
|
section: str = Field(
|
|
description=(
|
|
"One of goals, rules, preferences, or lessons_learned. Choose by whether the "
|
|
"content is an objective, a constraint, presentation guidance, or durable knowledge."
|
|
),
|
|
)
|
|
content: str = Field(
|
|
description=(
|
|
"One short sentence containing only reusable session guidance, not the full user "
|
|
"message or a one-off request."
|
|
),
|
|
)
|
|
confidence: float = Field(
|
|
default=0.0,
|
|
description="Confidence from 0 to 1. Only candidates with confidence >= 0.75 are stored.",
|
|
)
|
|
|
|
@field_validator("section")
|
|
@classmethod
|
|
def section_valid(cls, v: str) -> str:
|
|
if not isinstance(v, str):
|
|
raise ValueError("section must be a string")
|
|
normalized = v.strip().lower()
|
|
if normalized not in VALID_SECTIONS:
|
|
raise ValueError(f"section must be one of {sorted(VALID_SECTIONS)}")
|
|
return normalized
|
|
|
|
@field_validator("content")
|
|
@classmethod
|
|
def content_truncate(cls, v: str) -> str:
|
|
if not isinstance(v, str):
|
|
raise ValueError("content must be a string")
|
|
stripped = v.strip()
|
|
return stripped[:MAX_CONTEXT_CONTENT_CHARS]
|
|
|
|
@field_validator("confidence")
|
|
@classmethod
|
|
def confidence_clamp(cls, v: float) -> float:
|
|
if v is None:
|
|
return 0.0
|
|
try:
|
|
value = float(v)
|
|
except (TypeError, ValueError):
|
|
raise ValueError("confidence must be a number")
|
|
return max(0.0, min(1.0, value))
|
|
|
|
|
|
class CandidateGoalUpdate(CandidateContextUpdate):
|
|
"""Candidate update for the user's broader session objective."""
|
|
|
|
section: Literal["goals"] = Field(
|
|
default=ContextSection.GOALS.value,
|
|
description="Fixed section value for goal updates.",
|
|
)
|
|
content: str = Field(
|
|
description=(
|
|
"The user's broader objective or desired outcome for the session. Use this for where "
|
|
"the user is trying to get to, not constraints, facts, or answer format."
|
|
),
|
|
)
|
|
|
|
|
|
class CandidateRuleUpdate(CandidateContextUpdate):
|
|
"""Candidate update for substantive instructions future answers must follow."""
|
|
|
|
section: Literal["rules"] = Field(
|
|
default=ContextSection.RULES.value,
|
|
description="Fixed section value for rule updates.",
|
|
)
|
|
content: str = Field(
|
|
description=(
|
|
"An instruction future answers must obey: a constraint, requirement, assumption, "
|
|
"source boundary, decision criterion, technical choice, or thing to avoid. Use rules "
|
|
"for statements that change answer substance, even if phrased as facts."
|
|
),
|
|
)
|
|
|
|
|
|
class CandidatePreferenceUpdate(CandidateContextUpdate):
|
|
"""Candidate update for presentation and communication preferences."""
|
|
|
|
section: Literal["preferences"] = Field(
|
|
default=ContextSection.PREFERENCES.value,
|
|
description="Fixed section value for preference updates.",
|
|
)
|
|
content: str = Field(
|
|
description=(
|
|
"Presentation guidance only: style, tone, format, length, structure, terminology, "
|
|
"examples, ordering, or level of detail. Use preferences only when the substantive "
|
|
"answer would stay the same."
|
|
),
|
|
)
|
|
|
|
|
|
class CandidateLessonLearnedUpdate(CandidateContextUpdate):
|
|
"""Candidate update for session knowledge future answers should reason from."""
|
|
|
|
section: Literal["lessons_learned"] = Field(
|
|
default=ContextSection.LESSONS_LEARNED.value,
|
|
description="Fixed section value for lesson updates.",
|
|
)
|
|
content: str = Field(
|
|
description=(
|
|
"Durable knowledge future answers should reason from: a correction, clarification, "
|
|
"discovered fact, cause, outcome, or prior assumption update. It may overlap with a "
|
|
"rule when it captures reusable context behind that rule, but it must not be only a "
|
|
"direct instruction or required action."
|
|
),
|
|
)
|
|
|
|
|
|
CandidateContextUpdateVariant = Annotated[
|
|
CandidateGoalUpdate
|
|
| CandidateRuleUpdate
|
|
| CandidatePreferenceUpdate
|
|
| CandidateLessonLearnedUpdate,
|
|
Field(discriminator="section"),
|
|
]
|
|
|
|
|
|
class AgentCandidateContextUpdate(CandidateContextUpdate):
|
|
"""A proposed new agent-profile session-context lesson (tool/workflow guidance)."""
|
|
|
|
context_profile: Literal["agent"] = ContextProfile.AGENT.value
|
|
section: str = Field(
|
|
description=(
|
|
"One of tool_rules, workflow_state, success_patterns, failure_lessons, or "
|
|
"environment_facts. Choose by what kind of tool/workflow lesson the content is."
|
|
),
|
|
)
|
|
|
|
@field_validator("section")
|
|
@classmethod
|
|
def section_valid(cls, v: str) -> str:
|
|
if not isinstance(v, str):
|
|
raise ValueError("section must be a string")
|
|
normalized = v.strip().lower()
|
|
if normalized not in AGENT_VALID_SECTIONS:
|
|
raise ValueError(f"section must be one of {sorted(AGENT_VALID_SECTIONS)}")
|
|
return normalized
|
|
|
|
|
|
class CandidateToolRuleUpdate(AgentCandidateContextUpdate):
|
|
"""Candidate update for a reusable tool constraint or safe-invocation pattern."""
|
|
|
|
section: Literal["tool_rules"] = Field(
|
|
default=AgentContextSection.TOOL_RULES.value,
|
|
description="Fixed section value for tool-rule updates.",
|
|
)
|
|
content: str = Field(
|
|
description=(
|
|
"A reusable constraint or safe/required way to invoke a tool — what to do or avoid "
|
|
"when using it next time."
|
|
),
|
|
)
|
|
|
|
|
|
class CandidateWorkflowStateUpdate(AgentCandidateContextUpdate):
|
|
"""Candidate update for where the current task stands."""
|
|
|
|
section: Literal["workflow_state"] = Field(
|
|
default=AgentContextSection.WORKFLOW_STATE.value,
|
|
description="Fixed section value for workflow-state updates.",
|
|
)
|
|
content: str = Field(
|
|
description=(
|
|
"Where the current task stands and what must survive context compaction to continue "
|
|
"it — progress, the next step, or a pending decision."
|
|
),
|
|
)
|
|
|
|
|
|
class CandidateSuccessPatternUpdate(AgentCandidateContextUpdate):
|
|
"""Candidate update for an approach worth repeating."""
|
|
|
|
section: Literal["success_patterns"] = Field(
|
|
default=AgentContextSection.SUCCESS_PATTERNS.value,
|
|
description="Fixed section value for success-pattern updates.",
|
|
)
|
|
content: str = Field(
|
|
description="An approach or sequence that solved the user's request and is worth repeating.",
|
|
)
|
|
|
|
|
|
class CandidateFailureLessonUpdate(AgentCandidateContextUpdate):
|
|
"""Candidate update for something to avoid next time."""
|
|
|
|
section: Literal["failure_lessons"] = Field(
|
|
default=AgentContextSection.FAILURE_LESSONS.value,
|
|
description="Fixed section value for failure-lesson updates.",
|
|
)
|
|
content: str = Field(
|
|
description=(
|
|
"An error, retry, or rejected path and what to avoid or do differently next time."
|
|
),
|
|
)
|
|
|
|
|
|
class CandidateEnvironmentFactUpdate(AgentCandidateContextUpdate):
|
|
"""Candidate update for a discovered environment fact."""
|
|
|
|
section: Literal["environment_facts"] = Field(
|
|
default=AgentContextSection.ENVIRONMENT_FACTS.value,
|
|
description="Fixed section value for environment-fact updates.",
|
|
)
|
|
content: str = Field(
|
|
description=(
|
|
"A discovered fact about the repository, runtime, configuration, or environment that "
|
|
"future steps need."
|
|
),
|
|
)
|
|
|
|
|
|
AgentCandidateContextUpdateVariant = Annotated[
|
|
CandidateToolRuleUpdate
|
|
| CandidateWorkflowStateUpdate
|
|
| CandidateSuccessPatternUpdate
|
|
| CandidateFailureLessonUpdate
|
|
| CandidateEnvironmentFactUpdate,
|
|
Field(discriminator="section"),
|
|
]
|
|
|
|
|
|
class AgentContextExtraction(BaseModel):
|
|
"""LLM output for the batch pass: agent-profile lessons drawn from trace evidence."""
|
|
|
|
lessons: List[AgentCandidateContextUpdateVariant] = Field(
|
|
default_factory=list,
|
|
description=(
|
|
"Reusable agent/tool lessons drawn from the traces. Each item must be one of the "
|
|
"section-specific agent candidate types."
|
|
),
|
|
)
|
|
|
|
@field_validator("lessons", mode="before")
|
|
@classmethod
|
|
def normalize_lessons(cls, value):
|
|
if not isinstance(value, list):
|
|
return []
|
|
normalized = []
|
|
for item in value:
|
|
if isinstance(item, AgentCandidateContextUpdate):
|
|
item = item.model_dump()
|
|
if isinstance(item, dict):
|
|
item = dict(item)
|
|
section = item.get("section")
|
|
if isinstance(section, str):
|
|
item["section"] = section.strip().lower()
|
|
item.setdefault("context_profile", ContextProfile.AGENT.value)
|
|
normalized.append(item)
|
|
return normalized
|
|
|
|
|
|
class SessionContextEntry(BaseModel):
|
|
"""A stored active session-context lesson, tagged by profile (qa or agent)."""
|
|
|
|
id: str
|
|
section: str
|
|
context_profile: str = ContextProfile.QA.value
|
|
content: str
|
|
normalized_content: str = ""
|
|
confidence: float = 0.0
|
|
created_at: str
|
|
source_feedback_ids: List[str] = Field(default_factory=list)
|
|
source_trace_ids: List[str] = Field(default_factory=list)
|
|
helpful_count: int = 0
|
|
harmful_count: int = 0
|
|
priority: int = 0
|
|
last_served_at: Optional[str] = None
|
|
embedding: Optional[List[float]] = None
|
|
kind: Literal["context"] = "context"
|
|
|
|
@field_validator("id")
|
|
@classmethod
|
|
def id_non_empty(cls, v: str) -> str:
|
|
if not isinstance(v, str):
|
|
raise ValueError("id must be a string")
|
|
stripped = v.strip()
|
|
if not stripped:
|
|
raise ValueError("id must be a non-empty string")
|
|
return stripped
|
|
|
|
@field_validator("section")
|
|
@classmethod
|
|
def section_normalize(cls, v: str) -> str:
|
|
# Membership is profile-dependent, so only normalize here; the (profile, section)
|
|
# pair is validated in validate_section_for_profile once both fields are set.
|
|
if not isinstance(v, str):
|
|
raise ValueError("section must be a string")
|
|
return v.strip().lower()
|
|
|
|
@field_validator("context_profile")
|
|
@classmethod
|
|
def context_profile_valid(cls, v: str) -> str:
|
|
if not isinstance(v, str):
|
|
raise ValueError("context_profile must be a string")
|
|
normalized = v.strip().lower()
|
|
if normalized not in VALID_PROFILES:
|
|
raise ValueError(f"context_profile must be one of {sorted(VALID_PROFILES)}")
|
|
return normalized
|
|
|
|
@field_validator("content")
|
|
@classmethod
|
|
def content_non_empty_truncate(cls, v: str) -> str:
|
|
if not isinstance(v, str):
|
|
raise ValueError("content must be a string")
|
|
stripped = v.strip()
|
|
if not stripped:
|
|
raise ValueError("content must be a non-empty string")
|
|
return stripped[:MAX_CONTEXT_CONTENT_CHARS]
|
|
|
|
@field_validator("normalized_content")
|
|
@classmethod
|
|
def normalized_content_string(cls, v: str) -> str:
|
|
if v is None:
|
|
return ""
|
|
if not isinstance(v, str):
|
|
raise ValueError("normalized_content must be a string")
|
|
return normalize_content(v)
|
|
|
|
@field_validator("confidence")
|
|
@classmethod
|
|
def confidence_clamp(cls, v: float) -> float:
|
|
if v is None:
|
|
return 0.0
|
|
try:
|
|
value = float(v)
|
|
except (TypeError, ValueError):
|
|
raise ValueError("confidence must be a number")
|
|
return max(0.0, min(1.0, value))
|
|
|
|
@field_validator("helpful_count", "harmful_count")
|
|
@classmethod
|
|
def counts_non_negative(cls, v: int) -> int:
|
|
if not isinstance(v, int) or isinstance(v, bool):
|
|
raise ValueError("count must be an integer")
|
|
if v < 0:
|
|
raise ValueError("count must be >= 0")
|
|
return v
|
|
|
|
@field_validator("source_feedback_ids", "source_trace_ids")
|
|
@classmethod
|
|
def source_id_lists_only_strings(cls, v: List[str]) -> List[str]:
|
|
if not isinstance(v, list):
|
|
raise ValueError("source id list must be a list")
|
|
normalized = []
|
|
for item in v:
|
|
if not isinstance(item, str):
|
|
raise ValueError("source id list must contain only strings")
|
|
stripped = item.strip()
|
|
if stripped:
|
|
normalized.append(stripped)
|
|
return normalized
|
|
|
|
@model_validator(mode="after")
|
|
def derive_normalized_content(self):
|
|
if not self.normalized_content:
|
|
self.normalized_content = normalize_content(self.content)
|
|
return self
|
|
|
|
@model_validator(mode="after")
|
|
def validate_section_for_profile(self):
|
|
allowed = valid_sections_for(self.context_profile)
|
|
if self.section not in allowed:
|
|
raise ValueError(
|
|
f"section '{self.section}' is not valid for profile "
|
|
f"'{self.context_profile}'; allowed: {sorted(allowed)}"
|
|
)
|
|
return self
|
|
|
|
|
|
class SessionFeedbackEntry(BaseModel):
|
|
"""A stored feedback record describing how a turn rated/extended session context."""
|
|
|
|
id: str
|
|
created_at: str
|
|
raw_text: str
|
|
referenced_qa_ids: List[str] = Field(default_factory=list)
|
|
influencing_context_ids: List[str] = Field(default_factory=list)
|
|
candidate_context_entries: List[dict] = Field(default_factory=list)
|
|
kind: Literal["feedback"] = "feedback"
|
|
|
|
@field_validator("id")
|
|
@classmethod
|
|
def id_non_empty(cls, v: str) -> str:
|
|
if not isinstance(v, str):
|
|
raise ValueError("id must be a string")
|
|
stripped = v.strip()
|
|
if not stripped:
|
|
raise ValueError("id must be a non-empty string")
|
|
return stripped
|
|
|
|
@field_validator("referenced_qa_ids", "influencing_context_ids")
|
|
@classmethod
|
|
def id_lists_only_strings(cls, v: List[str]) -> List[str]:
|
|
if not isinstance(v, list):
|
|
raise ValueError("id list must be a list")
|
|
normalized = []
|
|
for item in v:
|
|
if not isinstance(item, str):
|
|
raise ValueError("id list must contain only strings")
|
|
stripped = item.strip()
|
|
if stripped:
|
|
normalized.append(stripped)
|
|
return normalized
|