165 lines
5.5 KiB
Python
165 lines
5.5 KiB
Python
"""Structured context workspace and capsule contracts for multi-turn QA."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import Any, Literal
|
|
|
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
|
|
|
|
def _clean_text(value: Any, *, limit: int = 240) -> str:
|
|
return " ".join(str(value or "").strip().split())[:limit]
|
|
|
|
|
|
class ResearchCandidateItem(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
candidate_ref: str = ""
|
|
display_name: str = ""
|
|
summary: str = ""
|
|
confidence: Literal["high", "medium", "low", "unknown"] = "unknown"
|
|
status: Literal["direct_competitor", "adjacent", "weak_candidate", "unknown"] = "unknown"
|
|
source_labels: list[str] = Field(default_factory=list)
|
|
source_urls: list[str] = Field(default_factory=list)
|
|
|
|
@field_validator("candidate_ref", "display_name", "summary", mode="before")
|
|
@classmethod
|
|
def _normalize_text(cls, value: Any) -> str:
|
|
return _clean_text(value, limit=400)
|
|
|
|
@field_validator("source_labels", "source_urls", mode="before")
|
|
@classmethod
|
|
def _normalize_list(cls, value: Any) -> list[str]:
|
|
rows = value if isinstance(value, list) else []
|
|
out: list[str] = []
|
|
for item in rows:
|
|
normalized = _clean_text(item, limit=240)
|
|
if normalized and normalized not in out:
|
|
out.append(normalized)
|
|
return out[:8]
|
|
|
|
|
|
class ResearchWorkspace(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
workspace_version: str = "research_workspace.v1"
|
|
workspace_id: str = ""
|
|
workspace_kind: Literal["external_research", "hybrid_research", "internal_research", "new_project_ideation"] = "external_research"
|
|
topic: str = ""
|
|
primary_object_ref: str | None = None
|
|
primary_object_id: int | None = None
|
|
primary_object_title: str | None = None
|
|
candidate_set_binding: str | None = None
|
|
candidate_items: list[ResearchCandidateItem] = Field(default_factory=list)
|
|
selected_candidate_refs: list[str] = Field(default_factory=list)
|
|
research_goal: str | None = None
|
|
analysis_dimensions: list[str] = Field(default_factory=list)
|
|
evidence_refs: list[str] = Field(default_factory=list)
|
|
confidence_state: Literal["high", "medium", "low", "unknown"] = "unknown"
|
|
collected_inputs: dict[str, Any] = Field(default_factory=dict)
|
|
open_questions: list[str] = Field(default_factory=list)
|
|
next_recommended_actions: list[str] = Field(default_factory=list)
|
|
status: Literal["active", "paused", "done"] = "active"
|
|
pmflow_ai_thread_workspace: dict[str, Any] = Field(default_factory=dict)
|
|
|
|
@field_validator(
|
|
"workspace_id",
|
|
"topic",
|
|
"primary_object_ref",
|
|
"primary_object_title",
|
|
"candidate_set_binding",
|
|
"research_goal",
|
|
mode="before",
|
|
)
|
|
@classmethod
|
|
def _normalize_field(cls, value: Any) -> str | None:
|
|
if value is None:
|
|
return None
|
|
return _clean_text(value, limit=400)
|
|
|
|
@field_validator(
|
|
"selected_candidate_refs",
|
|
"analysis_dimensions",
|
|
"evidence_refs",
|
|
"open_questions",
|
|
"next_recommended_actions",
|
|
mode="before",
|
|
)
|
|
@classmethod
|
|
def _normalize_text_list(cls, value: Any) -> list[str]:
|
|
rows = value if isinstance(value, list) else []
|
|
out: list[str] = []
|
|
for item in rows:
|
|
normalized = _clean_text(item, limit=240)
|
|
if normalized and normalized not in out:
|
|
out.append(normalized)
|
|
return out[:12]
|
|
|
|
@field_validator("collected_inputs", "pmflow_ai_thread_workspace", mode="before")
|
|
@classmethod
|
|
def _normalize_dict(cls, value: Any) -> dict[str, Any]:
|
|
return dict(value) if isinstance(value, dict) else {}
|
|
|
|
|
|
class ContextCapsule(BaseModel):
|
|
model_config = ConfigDict(extra="ignore")
|
|
|
|
capsule_version: str = "context_capsule.v1"
|
|
workspace_id: str = ""
|
|
workspace_kind: str = ""
|
|
topic: str = ""
|
|
primary_object_ref: str | None = None
|
|
candidate_set_binding: str | None = None
|
|
summary: str = ""
|
|
open_questions: list[str] = Field(default_factory=list)
|
|
next_action: str | None = None
|
|
|
|
@field_validator(
|
|
"workspace_id",
|
|
"workspace_kind",
|
|
"topic",
|
|
"primary_object_ref",
|
|
"candidate_set_binding",
|
|
"summary",
|
|
"next_action",
|
|
mode="before",
|
|
)
|
|
@classmethod
|
|
def _normalize_capsule_text(cls, value: Any) -> str | None:
|
|
if value is None:
|
|
return None
|
|
return _clean_text(value, limit=400)
|
|
|
|
@field_validator("open_questions", mode="before")
|
|
@classmethod
|
|
def _normalize_open_questions(cls, value: Any) -> list[str]:
|
|
rows = value if isinstance(value, list) else []
|
|
out: list[str] = []
|
|
for item in rows:
|
|
normalized = _clean_text(item, limit=240)
|
|
if normalized and normalized not in out:
|
|
out.append(normalized)
|
|
return out[:8]
|
|
|
|
|
|
def normalize_research_workspace_payload(value: Any) -> dict[str, Any]:
|
|
if not isinstance(value, dict) or not value:
|
|
return {}
|
|
try:
|
|
return ResearchWorkspace.model_validate(value).model_dump()
|
|
except Exception:
|
|
return {}
|
|
|
|
|
|
def normalize_context_capsules_payload(value: Any) -> list[dict[str, Any]]:
|
|
rows = value if isinstance(value, list) else []
|
|
out: list[dict[str, Any]] = []
|
|
for item in rows:
|
|
if not isinstance(item, dict):
|
|
continue
|
|
try:
|
|
out.append(ContextCapsule.model_validate(item).model_dump())
|
|
except Exception:
|
|
continue
|
|
return out[:8]
|