项目文件夹

文件
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:02:24 +08:00

429 行
15 KiB
Python

"""Processing one session turn: interpret the user message, apply its feedback to the
stored session context, and assemble the prompt for the answer.
These are the helpers behind SessionManager's turn flow. Like ``session_context_builder``,
they take the SessionManager as a parameter and call back into its storage facade, so
SessionManager stays an orchestrator plus a thin facade rather than holding this logic.
All public coroutines are fail-open so they never block answer generation.
"""
from dataclasses import dataclass, field
from datetime import datetime, timezone
from typing import Any
from uuid import uuid4
from cognee.context_global_variables import session_user
from cognee.infrastructure.session.feedback_detection import analyze_turn_for_session_context
from cognee.infrastructure.session.feedback_models import SessionTurnAnalysis
from cognee.infrastructure.session.session_context_builder import (
apply_candidate_updates,
build_active_context_block,
)
from cognee.infrastructure.session.session_context_models import SessionFeedbackEntry
from cognee.infrastructure.session.session_embeddings import (
merge_hybrid_qa_entries,
search_session_qa_ids,
)
from cognee.modules.retrieval.utils.completion import (
generate_session_completion_with_optional_summary,
)
from cognee.shared.logging_utils import get_logger
logger = get_logger("session_turn")
@dataclass
class SessionTurnPreparation:
"""Pre-answer decision and updates for one session turn."""
should_answer: bool = True
response_to_user: str | None = None
effective_query: str = ""
analysis: SessionTurnAnalysis | None = None
accepted_context_ids: list[str] = field(default_factory=list)
previous_qa_id: str | None = None
def compose_session_prompt(
active_context_block: str,
conversation_history: str,
) -> str:
"""Assemble the session prompt from active guidance and conversation history.
Empty layers are skipped. The active session-context block is placed before
the conversation history so durable user/session guidance remains prominent.
"""
prompt = conversation_history
if active_context_block:
prompt = active_context_block + "\n\n" + prompt
return prompt
def _empty_turn_preparation(query: str) -> SessionTurnPreparation:
return SessionTurnPreparation(should_answer=True, effective_query=query)
def coerce_qa_entry(entry: Any) -> dict:
"""Normalize a stored QA entry (model or dict) to a plain dict."""
if hasattr(entry, "model_dump"):
return entry.model_dump()
if isinstance(entry, dict):
return entry
return {}
async def select_session_history(
session_manager,
user_id: str,
session_id: str,
query_text: str,
) -> str:
"""Load session history and return it as a formatted conversation string.
History is the union of the last N turns and vector-engine hits, in
chronological order. On any failure, this falls back to the plain last-N window.
"""
try:
vector_qa_ids = await search_session_qa_ids(
user_id=user_id,
session_id=session_id,
query_text=query_text,
)
recent_entries = await session_manager.get_session(
user_id=user_id,
session_id=session_id,
formatted=False,
last_n=session_manager.session_history_last_n,
)
vector_entries = []
if vector_qa_ids:
vector_entries = await session_manager.get_session_entries_by_ids(
user_id=user_id,
session_id=session_id,
qa_ids=vector_qa_ids,
)
if isinstance(recent_entries, list) and isinstance(vector_entries, list):
selected = merge_hybrid_qa_entries(recent_entries, vector_entries)
return session_manager.format_entries(
[coerce_qa_entry(entry) for entry in selected],
include_context=False,
)
except Exception as error:
logger.warning("Session history: hybrid selection failed open: %s", error)
history = await session_manager.get_session(
user_id=user_id,
session_id=session_id,
formatted=True,
last_n=session_manager.session_history_last_n,
include_context=False,
)
return history if isinstance(history, str) else ""
async def generate_session_answer(
session_manager,
*,
user_id: str,
session_id: str,
answer_query: str,
context: str,
user_prompt_path: str,
system_prompt_path: str,
system_prompt: str | None,
response_model: type,
summarize_context: bool,
max_context_chars: int | None,
) -> tuple[Any, str, list[str] | None]:
"""Recall history and context, compose the prompt, and generate one answer.
Returns ``(answer, context_to_store, served_context_ids)``.
"""
conversation_history = await select_session_history(
session_manager,
user_id,
session_id,
query_text=answer_query,
)
served_ids: list[str] = []
active_context_block = ""
if session_manager.is_auto_feedback_enabled():
active_context_block, served_ids = await build_active_context_block_safe(
session_manager,
user_id=user_id,
session_id=session_id,
query=answer_query,
)
conversation_history = compose_session_prompt(active_context_block, conversation_history)
(
answer,
context_to_store,
_feedback_result,
) = await generate_session_completion_with_optional_summary(
query=answer_query,
context=context,
conversation_history=conversation_history,
user_prompt_path=user_prompt_path,
system_prompt_path=system_prompt_path,
system_prompt=system_prompt,
response_model=response_model,
summarize_context=summarize_context,
)
return answer, context_to_store, served_ids or None
async def build_active_context_block_safe(
session_manager,
*,
user_id: str,
session_id: str,
query: str,
) -> tuple[str, list[str]]:
"""Render the active session-context guidance block. Fail-open -> ("", [])."""
try:
return await build_active_context_block(
session_manager=session_manager,
user_id=user_id,
session_id=session_id,
query=query,
)
except Exception as e:
logger.warning("Active session-context block failed: %s", e)
return "", []
async def load_served_context_payload(
session_manager,
*,
user_id: str,
session_id: str,
served_ids: list[str],
) -> list[dict]:
"""Resolve the context entries served to the previous answer into {id, content} dicts.
These feed the single turn-analysis call so it can rate them. Fail-open -> [].
"""
if not served_ids:
return []
try:
entries = await session_manager.get_session_context_entries(
user_id=user_id, session_id=session_id
)
by_id = {}
for raw in entries or []:
row = raw if isinstance(raw, dict) else getattr(raw, "__dict__", {})
entry_id = row.get("id")
if entry_id is not None and row.get("kind", "context") == "context":
by_id[str(entry_id)] = row.get("content", "")
return [{"id": cid, "content": by_id[cid]} for cid in served_ids if cid in by_id]
except Exception as e:
logger.warning("Session turn: load served context failed: %s", e)
return []
async def apply_served_context_ratings(
session_manager,
*,
user_id: str,
session_id: str,
ratings: list,
) -> None:
"""Increment helpful_count / harmful_count for rated entries. Fail-open per rating."""
if not ratings:
return
try:
entries = await session_manager.get_session_context_entries(
user_id=user_id, session_id=session_id
)
counts = {}
for raw in entries or []:
row = raw if isinstance(raw, dict) else getattr(raw, "__dict__", {})
if row.get("kind", "context") != "context":
continue
entry_id = row.get("id")
if entry_id is not None:
counts[str(entry_id)] = (
int(row.get("helpful_count", 0) or 0),
int(row.get("harmful_count", 0) or 0),
)
for rating in ratings:
try:
entry_id = str(getattr(rating, "entry_id", None) or "")
verdict = str(getattr(rating, "rating", "") or "").strip().lower()
if entry_id not in counts or verdict not in ("helpful", "harmful"):
continue
helpful, harmful = counts[entry_id]
if verdict == "helpful":
merge = {"helpful_count": helpful + 1}
next_counts = (helpful + 1, harmful)
else:
merge = {"harmful_count": harmful + 1}
next_counts = (helpful, harmful + 1)
await session_manager.update_session_context_entry(
user_id=user_id,
entry_id=entry_id,
merge=merge,
session_id=session_id,
)
counts[entry_id] = next_counts
except Exception:
continue
except Exception as e:
logger.warning("Session turn: served-context rating update failed: %s", e)
async def apply_session_turn_analysis(
session_manager,
*,
user_id: str,
session_id: str,
query: str,
analysis: SessionTurnAnalysis,
previous_qa_id: str | None,
served_ids: list[str],
) -> list[str]:
"""Persist turn evidence, apply candidate updates, and bump helpful/harmful counters."""
if not analysis.candidate_context_updates and not analysis.served_context_ratings:
return []
try:
ratings = list(analysis.served_context_ratings or [])
candidates = list(analysis.candidate_context_updates or [])
feedback_entry = SessionFeedbackEntry(
id=str(uuid4()),
created_at=datetime.now(timezone.utc).isoformat(),
raw_text=query,
referenced_qa_ids=[previous_qa_id] if previous_qa_id else [],
influencing_context_ids=list(served_ids or []),
candidate_context_entries=[
c.model_dump() if hasattr(c, "model_dump") else dict(c) for c in candidates
],
)
await session_manager.create_session_context_entry(
user_id=user_id,
entry_dump=feedback_entry.model_dump(),
session_id=session_id,
)
touched_ids = await apply_candidate_updates(
session_manager=session_manager,
user_id=user_id,
session_id=session_id,
source_id=feedback_entry.id,
candidates=candidates,
)
await apply_served_context_ratings(
session_manager,
user_id=user_id,
session_id=session_id,
ratings=ratings,
)
return touched_ids
except Exception as e:
logger.warning("Session turn: feedback application failed: %s", e)
return []
async def prepare_session_turn(
session_manager,
*,
query: str,
session_id: str | None = None,
user_id: str | None = None,
) -> SessionTurnPreparation:
"""Analyze one user turn before retrieval/answer generation.
Runs only when caching and auto_feedback are enabled. Applies accepted candidate
guidance, rates previously served guidance, and returns the effective query that
retrieval and answer generation should use. Fail-open to a pass-through on any error.
"""
resolved_user_id = user_id
if resolved_user_id is None:
user = session_user.get()
resolved_user_id = getattr(user, "id", None)
if not session_manager.is_session_available_for_completion(resolved_user_id):
return _empty_turn_preparation(query)
if not session_manager.is_auto_feedback_enabled():
return _empty_turn_preparation(query)
resolved_session_id = session_manager._resolve_session_id(session_id)
try:
previous_entries = await session_manager.get_session(
user_id=str(resolved_user_id),
session_id=resolved_session_id,
formatted=False,
last_n=1,
)
previous_entry = (
coerce_qa_entry(previous_entries[-1])
if isinstance(previous_entries, list) and previous_entries
else {}
)
previous_qa_id = previous_entry.get("qa_id")
previous_question = previous_entry.get("question")
previous_answer = previous_entry.get("answer")
previous_served_ids = previous_entry.get("used_session_context_ids") or []
if not isinstance(previous_served_ids, list):
previous_served_ids = []
served_context = await load_served_context_payload(
session_manager,
user_id=str(resolved_user_id),
session_id=resolved_session_id,
served_ids=[str(entry_id) for entry_id in previous_served_ids],
)
analysis = await analyze_turn_for_session_context(
query,
previous_question=previous_question,
previous_answer=previous_answer,
served_context=served_context,
)
except Exception as error:
logger.warning("Session turn preparation failed open: %s", error)
return _empty_turn_preparation(query)
try:
accepted_context_ids = await apply_session_turn_analysis(
session_manager,
user_id=str(resolved_user_id),
session_id=resolved_session_id,
query=query,
analysis=analysis,
previous_qa_id=previous_qa_id,
served_ids=[str(entry_id) for entry_id in previous_served_ids],
)
except Exception as error:
logger.warning("Session turn analysis application failed open: %s", error)
accepted_context_ids = []
query_to_answer = (analysis.query_to_answer or "").strip()
response_to_user = (analysis.response_to_user or "").strip() or None
has_analysis_signal = bool(
query_to_answer
or response_to_user
or analysis.candidate_context_updates
or analysis.served_context_ratings
)
has_previous_answer = bool(previous_qa_id)
should_answer = bool(query_to_answer or not has_analysis_signal or not has_previous_answer)
effective_query = query_to_answer or query
if not should_answer and not response_to_user:
response_to_user = "Got it."
return SessionTurnPreparation(
should_answer=should_answer,
response_to_user=response_to_user,
effective_query=effective_query,
analysis=analysis,
accepted_context_ids=accepted_context_ids,
previous_qa_id=previous_qa_id,
)