"""Curator Agent — Policy curation engine
Intelligently integrates validated policy improvement hypotheses into the narrative policy document.
Core capabilities:
1. Principle integration: Weave new insights into the policy narrative, maintaining tone and coherence
2. Redundancy resolution: Merge similar principles, remove disproven beliefs
3. Structure optimization: Maintain narrative quality and organizational structure of the policy document
4. Change tracking: Record the changes and reasons for each curation
Design principles:
- Narrative consistency: New content must match the policy's narrative style and tone
- Minimal intrusion: No more than 3 substantive changes per curation cycle
- Reversibility: Record change history, support rollback
- Non-destructive: Do not remove proven, validated principles
"""
import json
import uuid
from datetime import datetime
from typing import Optional
from pathlib import Path
from openai import OpenAI
import os
from learning.schemas.evolution import Hypothesis
# Policy curation prompt
CURATION_PROMPT = """# Task: Curate the Trading Policy
You are a trading cognition curator. Your job is to integrate validated insights into the trader's playbook — a NARRATIVE document describing their trading philosophy, principles, and approach.
## Current Policy
{current_policy}
## Hypotheses to Integrate (ALL have passed backtest validation)
{hypotheses_text}
## Curation Guidelines
1. **Preserve what works**: Do NOT remove or weaken principles that have evidence of working. The policy represents hard-won trading wisdom.
2. **Integrate naturally**: New insights should be woven into the existing narrative, not appended as a separate section. They should feel like they always belonged.
3. **Maintain the voice**: The policy speaks as "I" — a trader describing their approach. Keep this voice consistent.
4. **Be specific and actionable**: Vague philosophy ("be careful") is useless. Each principle should describe WHAT to do and WHY.
5. **Limit scope**: Maximum 3 substantive changes per curation cycle. Small focused improvements beat large rewrites.
6. **Resolve contradictions**: If a new insight conflicts with an existing principle, reconcile them explicitly — don't leave contradictory advice.
7. **Remove disproven beliefs**: If a hypothesis was generated to fix a principle that led to consistent errors, that principle should be revised.
## Curation Actions
For each hypothesis, decide ONE of:
- **INTEGRATE**: Weave the insight into the appropriate section (entry philosophy, exit philosophy, position management, risk philosophy, or lessons learned)
- **MERGE**: Combine with an existing similar principle (if the insight overlaps with something already in the policy, strengthen the existing text)
- **REPLACE**: Replace an existing principle that has been shown to be flawed
- **REJECT**: Skip this insight (must provide clear reason — contradictory to core philosophy, too vague, already covered)
## Output Format (JSON)
```json
{{
"curation_result": {{
"curation_id": "cur_xxx",
"timestamp": "ISO timestamp",
"actions_taken": [
{{
"hypothesis_id": "hyp_xxx",
"action": "INTEGRATE | MERGE | REPLACE | REJECT",
"section_affected": "Which section of the policy was modified",
"reasoning": "Why this action was chosen",
"change_summary": "One sentence describing what changed"
}}
],
"updated_policy": "THE COMPLETE updated policy text in markdown",
"change_log_entry": "Concise description of what changed and why, for the version history"
}}
}}
```
**CRITICAL**:
- The `updated_policy` field must contain the COMPLETE policy text, not just the changed sections.
- Maintain ALL existing sections and their structure.
- The policy must remain a coherent narrative — not a bullet-point list of rules.
- Write all policy content in first-person voice ("I", "my", "we").
"""
class CuratorAgent:
"""Policy curation agent — integrates validated hypotheses into the narrative policy"""
def __init__(
self,
model: str = "deepseek-chat",
api_key: str = None,
api_base: str = None,
):
"""
Initialize the curation agent
Args:
model: LLM model name
api_key: API key (optional)
api_base: API base URL (optional)
"""
self.model = model
self.api_key = api_key or os.getenv("OPENAI_API_KEY")
self.api_base = api_base or os.getenv("OPENAI_API_BASE", "https://api.deepseek.com/v1")
self.llm_client = OpenAI(api_key=self.api_key, base_url=self.api_base, timeout=120.0)
def curate(
self,
current_policy: str,
hypotheses: list[Hypothesis],
policy_version: str = "v6",
) -> dict:
"""
Curate policy: integrate validated hypotheses into the current policy
Args:
current_policy: Current policy text (complete narrative playbook)
hypotheses: List of validated hypotheses
policy_version: Current policy version number
Returns:
dict with keys:
- curation_id: str
- actions_taken: list[dict]
- updated_policy: str (complete updated policy text)
- change_log: str
- success: bool
"""
curation_id = f"cur_{uuid.uuid4().hex[:12]}"
if not hypotheses:
return {
"curation_id": curation_id,
"actions_taken": [],
"updated_policy": current_policy,
"change_log": "No hypotheses to integrate",
"success": True,
}
# Format hypotheses
hypotheses_text = self._format_hypotheses(hypotheses)
# Build prompt
prompt = CURATION_PROMPT.format(
current_policy=current_policy,
hypotheses_text=hypotheses_text,
)
# Call LLM
try:
response = self.llm_client.chat.completions.create(
model=self.model,
messages=[
{
"role": "system",
"content": (
"You are a trading cognition curator. You maintain a trader's playbook — "
"a narrative document describing their trading philosophy, principles, and "
"lessons learned. You integrate new validated insights while preserving the "
"document's coherence, voice, and proven wisdom. You write in the first-person "
"voice of the trader. You never delete proven principles without good reason."
),
},
{"role": "user", "content": prompt},
],
response_format={"type": "json_object"},
temperature=0.4,
)
result = json.loads(response.choices[0].message.content)
curation = result.get("curation_result", result)
return {
"curation_id": curation_id,
"actions_taken": curation.get("actions_taken", []),
"updated_policy": curation.get("updated_policy", current_policy),
"change_log": curation.get("change_log_entry", "Policy updated via curation"),
"success": True,
}
except Exception as e:
print(f"Error in curation: {e}")
return {
"curation_id": curation_id,
"actions_taken": [],
"updated_policy": current_policy,
"change_log": f"Curation failed: {e}",
"success": False,
}
def curate_incremental(
self,
current_policy: str,
new_insight: str,
insight_source: str = "reflection",
policy_version: str = "v6",
) -> dict:
"""
Incremental curation: integrate a single insight into the policy (lightweight version for daily small updates)
Args:
current_policy: Current policy text
new_insight: New trading insight (narrative description)
insight_source: Insight source (reflection / manual / experiment)
policy_version: Current policy version
Returns:
dict with keys: curation_id, updated_policy, change_log, success
"""
# Wrap the single insight as a simplified hypothesis
prompt = f"""# Task: Integrate a New Trading Insight
You maintain a trader's narrative playbook. Integrate the following new insight into the policy naturally.
## Current Policy
{current_policy}
## New Insight (from {insight_source})
{new_insight}
## Instructions
1. Read the insight and find the most natural place for it in the policy
2. Weave it into the existing narrative — do NOT append as a separate section
3. If the insight overlaps with existing content, strengthen/refine the existing text
4. If the insight is not actionable or too vague, integrate it into "Lessons Learned"
5. Maintain first-person voice throughout
Output JSON:
```json
{{
"curation": {{
"updated_policy": "COMPLETE updated policy text",
"change_log": "One sentence describing what was changed",
"section_affected": "Which section was modified"
}}
}}
```"""
try:
response = self.llm_client.chat.completions.create(
model=self.model,
messages=[
{
"role": "system",
"content": "You are a trading cognition curator. Integrate new insights into the trader's narrative playbook naturally.",
},
{"role": "user", "content": prompt},
],
response_format={"type": "json_object"},
temperature=0.3,
)
result = json.loads(response.choices[0].message.content)
curation = result.get("curation", result)
curation_id = f"cur_inc_{uuid.uuid4().hex[:12]}"
return {
"curation_id": curation_id,
"updated_policy": curation.get("updated_policy", current_policy),
"change_log": curation.get("change_log", f"Integrated insight from {insight_source}"),
"section_affected": curation.get("section_affected", "unknown"),
"success": True,
}
except Exception as e:
print(f"Error in incremental curation: {e}")
return {
"curation_id": f"cur_inc_{uuid.uuid4().hex[:12]}",
"updated_policy": current_policy,
"change_log": f"Incremental curation failed: {e}",
"success": False,
}
def validate_policy_structure(self, policy_text: str) -> dict:
"""
Validate policy document structure integrity
Checks whether the policy contains core sections without mandating a fixed structure.
Args:
policy_text: Policy text
Returns:
dict with validation results
"""
expected_sections = [
"who i am", "trader",
"market", "read", "environment",
"entry", "enter",
"position", "manage",
"exit", "leave",
"risk",
"lesson",
]
text_lower = policy_text.lower()
found = [s for s in expected_sections if s in text_lower]
missing = [s for s in expected_sections if s not in text_lower]
# Lenient check: structure is considered complete if at least half the keywords are found
is_valid = len(found) >= len(expected_sections) // 2
return {
"is_valid": is_valid,
"sections_found": found,
"keywords_missing": missing,
"score": len(found) / len(expected_sections),
}
def _format_hypotheses(self, hypotheses: list[Hypothesis]) -> str:
"""Format hypothesis list as text"""
lines = []
for i, h in enumerate(hypotheses, 1):
lines.append(f"### Hypothesis {i}: {h.description}")
lines.append(f"**Type**: {h.hypothesis_type}")
lines.append(f"**Expected Benefit**: {h.expected_benefit}")
lines.append(f"**Risk**: {h.risk}")
lines.append(f"**Proposed Change**:")
if isinstance(h.proposed_change, dict):
change_type = h.proposed_change.get("type", "unknown")
narrative = h.proposed_change.get("narrative", str(h.proposed_change))
lines.append(f" - Change type: {change_type}")
lines.append(f" - Narrative text: {narrative}")
else:
lines.append(f" {h.proposed_change}")
_hc = getattr(h, 'confidence', None) or 0
lines.append(f"**Confidence**: {_hc:.2f}")
lines.append("")
return "\n".join(lines)
Workflows from the Neura Market marketplace related to this DeepSeek resource