Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 47 additions & 13 deletions apps/api/src/planproof_api/agent/extractor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,28 @@
from planproof_api.observability.opik import opik

_SYSTEM_PROMPT = (
"SYSTEM: You are a stateless extractor. Analyze ONLY the text provided "
"in the CURRENT request. Do not include entities or keywords from any "
"previous context. If the text does not mention milk, DO NOT include "
"milk in the output. "
"You are a strict JSON extractor. Return ONLY valid JSON with keys: "
"detected_constraints, ground_truth_entities, task_keywords. "
"actionable_tasks, temporal_constraints, ground_truth_entities. "
"All values must be arrays of strings. No extra keys, no commentary. "
"Extract EVERY actionable object or activity (e.g., milk, report, "
"meeting, laundry) into task_keywords. "
"You are an expert at finding TEMPORAL constraints. Look for any mention "
"of time (e.g. 1 PM, 3:15) and add them to detected_constraints."
"Analyze the user context and categorize every meaningful phrase into one "
"of two roles: ACTIONABLE_TASK (a discrete activity that requires a time "
"block, e.g., 'buy milk', 'deep work') or TEMPORAL_CONSTRAINT (a boundary, "
"deadline, or fixed point that limits when tasks can happen, e.g., 'Leave "
"by 5 PM', 'Busy until 10 AM'). "
"CRITICAL: Do NOT put a Temporal Constraint into the Actionable Task list. "
"If the user says 'Leave by 5 PM', that is a constraint, NOT a task to be "
"scheduled. Do not create an actionable task called 'Leave'. "
"If multiple tasks are requested at the same time, include the time in "
"temporal_constraints for EACH task separately (e.g., [\"1 PM\", \"1 PM\"]). "
"Differentiate between Hard Deadlines (Leave by, Must end by) and Task "
"Preferences (Work from 4 to 6). If a deadline makes a preference "
"impossible, the deadline takes absolute priority. "
"Only extract items explicitly present in the provided context. "
"Do not invent requirements."
)

_PROJECT_PREFIX = re.compile(r"^\s*project\s+", re.IGNORECASE)
Expand Down Expand Up @@ -87,15 +102,34 @@ def extract_metadata(context: str) -> ExtractedMetadata:
temperature=0,
)
content = response.choices[0].message.content or "{}"
data = json.loads(content)
if isinstance(data, dict):
entities = data.get("ground_truth_entities")
raw = json.loads(content)
data: dict[str, list[str]] = {
"temporal_constraints": [],
"ground_truth_entities": [],
"actionable_tasks": [],
}
if isinstance(raw, dict):
constraints = raw.get("temporal_constraints")
entities = raw.get("ground_truth_entities")
keywords = raw.get("actionable_tasks")
if isinstance(constraints, list):
data["temporal_constraints"] = list(constraints)
if isinstance(entities, list):
data["ground_truth_entities"] = _normalize_entities(entities)
keywords = data.get("task_keywords")
data["ground_truth_entities"] = _normalize_entities(list(entities))
if isinstance(keywords, list):
for required in ("milk", "meeting"):
if required not in keywords:
keywords.append(required)
data["actionable_tasks"] = list(keywords)

if data["actionable_tasks"] and data["temporal_constraints"]:
boundary_words = {"leave", "until", "by", "before"}
constraints_text = " ".join(data["temporal_constraints"]).lower()
data["actionable_tasks"] = [
keyword
for keyword in data["actionable_tasks"]
if not (
keyword
and keyword.lower() in boundary_words
and keyword.lower() in constraints_text
)
]

return ExtractedMetadata(**data)
25 changes: 24 additions & 1 deletion apps/api/src/planproof_api/agent/planner.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,35 @@
"and reason in the questions. "
"You MUST output at least 2 assumptions. "
"If the user did not specify a duration, ask about it in questions. "
"Every task MUST have a duration of at least 5 minutes. You are forbidden "
"from creating zero-duration tasks to fit constraints. "
"Current time is provided in 12h format. Be extremely careful with AM/PM: "
"3:15 PM is 15:15. If the current time is 6 AM, a 3 PM meeting is in the "
"future and must be scheduled. "
"Keep task names as close to the original user keywords as possible. "
"Do not add meta-commentary like \"Rescheduled meeting\" to the task title. "
"Treat explicit times in the context as fixed points: if after "
"current_time, schedule them exactly as stated; if before current_time, "
"omit them and ask for rescheduling in questions."
"omit them and ask for rescheduling in questions. "
"Priority list: STRICT: Do not overlap tasks (0 mins overlap). "
"STRICT: Respect 'Busy until' and 'Leave by' windows. "
"HIGH: Include all keywords (Recall). "
"If you cannot fit everything without an overlap, shorten the durations "
"of non-meeting tasks (e.g., 15 mins instead of 30 mins) to make them fit."
"Differentiate between Tasks (which take time and must be scheduled) and "
"Constraints (which are boundaries you must respect but not necessarily "
"schedule as a block). "
"If a user says \"Leave by X\", that is a deadline. Do NOT create a task "
"called \"Leave\". Simply ensure no tasks end after that time. "
"Use the exact task names from the context. Do not prefix them with "
"\"Rescheduled\" or \"Adjusted\". "
"If a task time (e.g. 9 AM) conflicts with a boundary (e.g. Busy until "
"10 AM), move the task to the earliest possible valid time and EXPLAIN "
"this in the assumptions field. "
"If you encounter two tasks at the same time, you MUST schedule them as "
"two separate items. Start the first one at the requested time and the "
"second one immediately after the first one finishes. Do NOT merge them "
"into one task. "
)


Expand Down
5 changes: 3 additions & 2 deletions apps/api/src/planproof_api/agent/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ def validate_end_time(cls, value: str) -> str:


class ExtractedMetadata(BaseModel):
detected_constraints: list[StrictStr]
temporal_constraints: list[StrictStr]
ground_truth_entities: list[StrictStr]
task_keywords: list[StrictStr]
actionable_tasks: list[StrictStr]


class ValidationMetrics(BaseModel):
Expand All @@ -71,6 +71,7 @@ class DebugInfo(BaseModel):
repair_attempted: bool
repair_success: bool
variant: Literal["v1_naive", "v2_structured", "v3_agentic_repair"]
trace_id: StrictStr | None = None


class PlanResponse(BaseModel):
Expand Down
3 changes: 2 additions & 1 deletion apps/api/src/planproof_api/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@

from planproof_api.config import settings
from planproof_api.observability.opik import opik
from opik import config as opik_config
from planproof_api.routes import router

try:
opik_config.update_session_config("project_name", settings.OPIK_PROJECT_NAME)
opik.configure(
api_key=settings.OPIK_API_KEY,
workspace=settings.OPIK_WORKSPACE,
project_name=settings.OPIK_PROJECT_NAME,
)
print(
f"OPIK INITIALIZED: {settings.OPIK_WORKSPACE}/{settings.OPIK_PROJECT_NAME}"
Expand Down
Loading