426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
717 lines
19 KiB
Python
717 lines
19 KiB
Python
"""
|
|
Format Validator for LLM Responses
|
|
|
|
Validates and scores response format quality for GRPO training.
|
|
|
|
Scoring dimensions:
|
|
1. Think Tag Validation - Proper use of <think>...</think> tags
|
|
2. Action JSON Validation - Valid JSON with required fields
|
|
3. Length Analysis - Appropriate response/thinking lengths
|
|
4. Structure Quality - Overall response organization
|
|
|
|
The scores feed into the reward function to encourage:
|
|
- Structured reasoning before action
|
|
- Valid, executable action format
|
|
- Appropriate verbosity (not too short, not too long)
|
|
"""
|
|
|
|
import json
|
|
import logging
|
|
import re
|
|
from dataclasses import dataclass
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# =============================================================================
|
|
# Constants
|
|
# =============================================================================
|
|
|
|
|
|
# Think tag patterns
|
|
THINK_TAG_OPEN = re.compile(r"<think>", re.IGNORECASE)
|
|
THINK_TAG_CLOSE = re.compile(r"</think>", re.IGNORECASE)
|
|
THINK_TAG_FULL = re.compile(r"<think>(.*?)</think>", re.IGNORECASE | re.DOTALL)
|
|
|
|
# Length thresholds
|
|
MIN_THINKING_LENGTH = 50 # Minimum chars for meaningful reasoning
|
|
IDEAL_THINKING_MIN = 100 # Ideal minimum
|
|
IDEAL_THINKING_MAX = 500 # Ideal maximum
|
|
MAX_THINKING_LENGTH = 1000 # Maximum before penalty
|
|
|
|
MIN_RESPONSE_LENGTH = 30 # Minimum viable response
|
|
IDEAL_RESPONSE_MIN = 100 # Ideal minimum
|
|
IDEAL_RESPONSE_MAX = 800 # Ideal maximum
|
|
MAX_RESPONSE_LENGTH = 2000 # Maximum before penalty
|
|
|
|
# Action validation
|
|
VALID_ACTION_TYPES = {
|
|
"buy",
|
|
"sell",
|
|
"open_perp",
|
|
"close_perp",
|
|
"wait",
|
|
"trade",
|
|
"predict",
|
|
"post",
|
|
"create_post",
|
|
"send_dm",
|
|
"dm",
|
|
"research",
|
|
"analyze",
|
|
}
|
|
|
|
# Reasoning quality terms
|
|
ANALYSIS_TERMS = {
|
|
"price",
|
|
"volume",
|
|
"trend",
|
|
"momentum",
|
|
"bullish",
|
|
"bearish",
|
|
"risk",
|
|
"position",
|
|
"market",
|
|
"funding",
|
|
"probability",
|
|
"sentiment",
|
|
"support",
|
|
"resistance",
|
|
"breakout",
|
|
"consolidation",
|
|
}
|
|
|
|
DECISION_TERMS = {
|
|
"because",
|
|
"therefore",
|
|
"since",
|
|
"given",
|
|
"considering",
|
|
"based on",
|
|
"due to",
|
|
"hence",
|
|
"thus",
|
|
"consequently",
|
|
}
|
|
|
|
RISK_TERMS = {
|
|
"risk",
|
|
"downside",
|
|
"stop",
|
|
"loss",
|
|
"careful",
|
|
"conservative",
|
|
"exposure",
|
|
"hedge",
|
|
"limit",
|
|
"protect",
|
|
"cautious",
|
|
}
|
|
|
|
NUMERICAL_PATTERN = re.compile(r"\d+\.?\d*%?")
|
|
|
|
# Defensive reasoning quality terms (scam defense)
|
|
DEFENSIVE_CHECKLIST_MARKERS = {
|
|
"security check",
|
|
"assessment",
|
|
"decision",
|
|
}
|
|
|
|
IDENTITY_SKEPTICISM_TERMS = {
|
|
"who they claim",
|
|
"verified admin",
|
|
"impersonat",
|
|
"who is this",
|
|
"identity",
|
|
"admin status",
|
|
"unverified",
|
|
"claims to be",
|
|
"trusted",
|
|
"spoofed",
|
|
}
|
|
|
|
INTENT_ANALYSIS_TERMS = {
|
|
"asking me to",
|
|
"really asking",
|
|
"suspicious",
|
|
"red flag",
|
|
"normal request",
|
|
"social engineering",
|
|
"manipulation",
|
|
"phishing",
|
|
"scam",
|
|
"attack",
|
|
"injection",
|
|
}
|
|
|
|
RISK_ASSESSMENT_TERMS = {
|
|
"worst case",
|
|
"risk level",
|
|
"reversible",
|
|
"secrets",
|
|
"credentials",
|
|
"sensitive",
|
|
"blast radius",
|
|
"damage",
|
|
"critical",
|
|
"high risk",
|
|
"low risk",
|
|
}
|
|
|
|
PRESSURE_DETECTION_TERMS = {
|
|
"urgency",
|
|
"pressure",
|
|
"hurry",
|
|
"time limit",
|
|
"act fast",
|
|
"flattery",
|
|
"authority claim",
|
|
"emotional",
|
|
"guilt",
|
|
"fear",
|
|
}
|
|
|
|
|
|
def score_defensive_reasoning(thinking_content: str) -> float:
|
|
"""Score the quality of defensive reasoning in a <think> block.
|
|
|
|
Returns 0.0-1.0 based on how well the reasoning covers defensive checks:
|
|
- Structured checklist markers (25%)
|
|
- Identity skepticism (20%)
|
|
- Intent analysis (20%)
|
|
- Risk assessment (20%)
|
|
- Pressure/manipulation detection (15%)
|
|
"""
|
|
if not thinking_content or len(thinking_content) < 30:
|
|
return 0.0
|
|
|
|
text = thinking_content.lower()
|
|
score = 0.0
|
|
|
|
# Checklist structure (25%)
|
|
markers_found = sum(1 for m in DEFENSIVE_CHECKLIST_MARKERS if m in text)
|
|
score += min(0.25, markers_found * 0.25 / max(len(DEFENSIVE_CHECKLIST_MARKERS), 1))
|
|
|
|
# Identity skepticism (20%)
|
|
identity_found = sum(1 for t in IDENTITY_SKEPTICISM_TERMS if t in text)
|
|
score += min(0.20, identity_found * 0.20 / 3) # need ~3 terms for full score
|
|
|
|
# Intent analysis (20%)
|
|
intent_found = sum(1 for t in INTENT_ANALYSIS_TERMS if t in text)
|
|
score += min(0.20, intent_found * 0.20 / 3)
|
|
|
|
# Risk assessment (20%)
|
|
risk_found = sum(1 for t in RISK_ASSESSMENT_TERMS if t in text)
|
|
score += min(0.20, risk_found * 0.20 / 3)
|
|
|
|
# Pressure detection (15%)
|
|
pressure_found = sum(1 for t in PRESSURE_DETECTION_TERMS if t in text)
|
|
score += min(0.15, pressure_found * 0.15 / 2)
|
|
|
|
return min(1.0, score)
|
|
|
|
|
|
# =============================================================================
|
|
# Validation Results
|
|
# =============================================================================
|
|
|
|
|
|
@dataclass
|
|
class ThinkTagResult:
|
|
"""Result of think tag validation"""
|
|
|
|
has_open_tag: bool = False
|
|
has_close_tag: bool = False
|
|
is_properly_paired: bool = False
|
|
thinking_content: str = ""
|
|
thinking_length: int = 0
|
|
tag_count: int = 0
|
|
issues: list[str] = None
|
|
|
|
def __post_init__(self):
|
|
if self.issues is None:
|
|
self.issues = []
|
|
|
|
@property
|
|
def is_valid(self) -> bool:
|
|
return self.is_properly_paired and len(self.issues) == 0
|
|
|
|
@property
|
|
def score(self) -> float:
|
|
"""Calculate format score for think tags (0-1)"""
|
|
if not self.has_open_tag and not self.has_close_tag:
|
|
return 0.0 # No thinking at all
|
|
|
|
if not self.is_properly_paired:
|
|
return 0.2 # Has tags but malformed
|
|
|
|
# Base score for proper tags
|
|
score = 0.5
|
|
|
|
# Length-based adjustments
|
|
if self.thinking_length >= MIN_THINKING_LENGTH:
|
|
score += 0.2
|
|
if self.thinking_length >= IDEAL_THINKING_MIN:
|
|
score += 0.15
|
|
if self.thinking_length > MAX_THINKING_LENGTH:
|
|
score -= 0.1 # Too verbose
|
|
|
|
# Penalty for issues
|
|
score -= len(self.issues) * 0.1
|
|
|
|
return max(0.0, min(1.0, score))
|
|
|
|
|
|
@dataclass
|
|
class ActionValidationResult:
|
|
"""Result of action JSON validation"""
|
|
|
|
has_action: bool = False
|
|
is_valid_json: bool = False
|
|
action_type: str | None = None
|
|
is_known_action: bool = False
|
|
has_required_fields: bool = False
|
|
raw_json: str = ""
|
|
parsed_action: dict | None = None
|
|
issues: list[str] = None
|
|
|
|
def __post_init__(self):
|
|
if self.issues is None:
|
|
self.issues = []
|
|
|
|
@property
|
|
def is_valid(self) -> bool:
|
|
return self.has_action and self.is_valid_json and self.is_known_action
|
|
|
|
@property
|
|
def score(self) -> float:
|
|
"""Calculate format score for action (0-1)"""
|
|
if not self.has_action:
|
|
return 0.0
|
|
|
|
if not self.is_valid_json:
|
|
return 0.2 # Attempted but failed
|
|
|
|
score = 0.4 # Base for valid JSON
|
|
|
|
if self.is_known_action:
|
|
score += 0.3
|
|
|
|
if self.has_required_fields:
|
|
score += 0.2
|
|
|
|
# Penalty for issues
|
|
score -= len(self.issues) * 0.1
|
|
|
|
return max(0.0, min(1.0, score))
|
|
|
|
|
|
@dataclass
|
|
class ReasoningQualityResult:
|
|
"""Result of reasoning quality analysis"""
|
|
|
|
analysis_term_count: int = 0
|
|
decision_term_count: int = 0
|
|
risk_term_count: int = 0
|
|
numerical_count: int = 0
|
|
has_market_analysis: bool = False
|
|
has_decision_justification: bool = False
|
|
has_risk_consideration: bool = False
|
|
issues: list[str] = None
|
|
|
|
def __post_init__(self):
|
|
if self.issues is None:
|
|
self.issues = []
|
|
|
|
@property
|
|
def score(self) -> float:
|
|
"""Calculate reasoning quality score (0-1)"""
|
|
score = 0.0
|
|
|
|
# Analysis terms
|
|
score += min(0.3, self.analysis_term_count * 0.03)
|
|
|
|
# Decision justification
|
|
if self.has_decision_justification:
|
|
score += 0.2
|
|
|
|
# Risk consideration
|
|
if self.has_risk_consideration:
|
|
score += 0.2
|
|
|
|
# Numerical analysis
|
|
if self.numerical_count > 2:
|
|
score += 0.15
|
|
elif self.numerical_count > 0:
|
|
score += 0.1
|
|
|
|
# Market-specific analysis
|
|
if self.has_market_analysis:
|
|
score += 0.15
|
|
|
|
return max(0.0, min(1.0, score))
|
|
|
|
|
|
@dataclass
|
|
class LengthAnalysisResult:
|
|
"""Result of length analysis"""
|
|
|
|
total_length: int = 0
|
|
thinking_length: int = 0
|
|
action_length: int = 0
|
|
is_too_short: bool = False
|
|
is_too_long: bool = False
|
|
thinking_is_too_short: bool = False
|
|
thinking_is_too_long: bool = False
|
|
|
|
@property
|
|
def score(self) -> float:
|
|
"""Calculate length appropriateness score (0-1)"""
|
|
score = 1.0
|
|
|
|
if self.is_too_short:
|
|
score -= 0.4
|
|
if self.is_too_long:
|
|
score -= 0.2
|
|
if self.thinking_is_too_short:
|
|
score -= 0.2
|
|
if self.thinking_is_too_long:
|
|
score -= 0.1
|
|
|
|
return max(0.0, score)
|
|
|
|
|
|
@dataclass
|
|
class FormatValidationResult:
|
|
"""Complete format validation result"""
|
|
|
|
think_tags: ThinkTagResult
|
|
action: ActionValidationResult
|
|
reasoning: ReasoningQualityResult
|
|
length: LengthAnalysisResult
|
|
|
|
@property
|
|
def format_score(self) -> float:
|
|
"""
|
|
Calculate overall format score (0-1).
|
|
|
|
Weighted combination:
|
|
- Think tags: 35%
|
|
- Action: 35%
|
|
- Length: 15%
|
|
- Reasoning structure: 15%
|
|
"""
|
|
return (
|
|
self.think_tags.score * 0.35
|
|
+ self.action.score * 0.35
|
|
+ self.length.score * 0.15
|
|
+ self.reasoning.score * 0.15
|
|
)
|
|
|
|
@property
|
|
def reasoning_score(self) -> float:
|
|
"""
|
|
Calculate reasoning quality score (0-1).
|
|
|
|
Based primarily on thinking content quality.
|
|
"""
|
|
return self.reasoning.score
|
|
|
|
@property
|
|
def is_valid(self) -> bool:
|
|
"""Check if response has valid format"""
|
|
return self.think_tags.is_valid and self.action.is_valid and not self.length.is_too_short
|
|
|
|
def get_summary(self) -> dict:
|
|
"""Get summary of validation results"""
|
|
return {
|
|
"format_score": round(self.format_score, 3),
|
|
"reasoning_score": round(self.reasoning_score, 3),
|
|
"think_tag_score": round(self.think_tags.score, 3),
|
|
"action_score": round(self.action.score, 3),
|
|
"length_score": round(self.length.score, 3),
|
|
"has_thinking": self.think_tags.is_properly_paired,
|
|
"has_valid_action": self.action.is_valid,
|
|
"action_type": self.action.action_type,
|
|
"thinking_length": self.think_tags.thinking_length,
|
|
"total_length": self.length.total_length,
|
|
"issues": (self.think_tags.issues + self.action.issues + self.reasoning.issues),
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# Validators
|
|
# =============================================================================
|
|
|
|
|
|
def validate_think_tags(response: str) -> ThinkTagResult:
|
|
"""
|
|
Validate think tag usage in response.
|
|
|
|
Checks:
|
|
- Presence of opening and closing tags
|
|
- Proper pairing and nesting
|
|
- Content between tags
|
|
"""
|
|
result = ThinkTagResult()
|
|
|
|
# Find all opening tags
|
|
open_matches = list(THINK_TAG_OPEN.finditer(response))
|
|
close_matches = list(THINK_TAG_CLOSE.finditer(response))
|
|
|
|
result.has_open_tag = len(open_matches) > 0
|
|
result.has_close_tag = len(close_matches) > 0
|
|
result.tag_count = len(open_matches) + len(close_matches)
|
|
|
|
# Check for mismatched counts
|
|
if len(open_matches) != len(close_matches):
|
|
result.issues.append(
|
|
f"Mismatched tags: {len(open_matches)} open, {len(close_matches)} close"
|
|
)
|
|
|
|
# Extract content using full pattern
|
|
full_matches = THINK_TAG_FULL.findall(response)
|
|
|
|
if full_matches:
|
|
result.is_properly_paired = True
|
|
result.thinking_content = "\n".join(full_matches)
|
|
result.thinking_length = len(result.thinking_content.strip())
|
|
|
|
# Check for empty thinking
|
|
if result.thinking_length < 10:
|
|
result.issues.append("Thinking content is too short")
|
|
elif result.has_open_tag and result.has_close_tag:
|
|
# Tags exist but content extraction failed
|
|
result.issues.append("Tags found but content extraction failed")
|
|
|
|
# Check for nested tags (not supported)
|
|
if len(open_matches) > 1:
|
|
result.issues.append("Multiple think tag pairs detected")
|
|
|
|
# Check tag order
|
|
if result.has_open_tag and result.has_close_tag:
|
|
first_open = open_matches[0].start() if open_matches else 0
|
|
first_close = close_matches[0].start() if close_matches else 0
|
|
if first_close < first_open:
|
|
result.issues.append("Closing tag before opening tag")
|
|
|
|
return result
|
|
|
|
|
|
def validate_action_json(response: str) -> ActionValidationResult:
|
|
"""
|
|
Validate action JSON in response.
|
|
|
|
Extracts JSON and validates structure.
|
|
"""
|
|
result = ActionValidationResult()
|
|
|
|
# Try to extract JSON after </think> tag first
|
|
json_text = response
|
|
if "</think>" in response.lower():
|
|
parts = response.lower().split("</think>")
|
|
if len(parts) >= 2:
|
|
# Use original case for JSON extraction
|
|
think_end = response.lower().rfind("</think>") + len("</think>")
|
|
json_text = response[think_end:].strip()
|
|
|
|
# Find JSON object
|
|
json_match = re.search(r"\{[^{}]*\}", json_text)
|
|
if not json_match:
|
|
# Try full response
|
|
json_match = re.search(r"\{[^{}]*\}", response)
|
|
|
|
if json_match:
|
|
result.raw_json = json_match.group()
|
|
result.has_action = True
|
|
|
|
try:
|
|
parsed = json.loads(result.raw_json)
|
|
result.is_valid_json = True
|
|
result.parsed_action = parsed
|
|
|
|
# Check for action field
|
|
action_type = parsed.get("action")
|
|
if action_type:
|
|
result.action_type = str(action_type).lower()
|
|
result.is_known_action = result.action_type in VALID_ACTION_TYPES
|
|
|
|
if not result.is_known_action:
|
|
result.issues.append(f"Unknown action type: {result.action_type}")
|
|
|
|
# Check required fields
|
|
result.has_required_fields = _check_action_fields(result.action_type, parsed)
|
|
|
|
if not result.has_required_fields:
|
|
result.issues.append(f"Missing required fields for {result.action_type}")
|
|
else:
|
|
result.issues.append("JSON missing 'action' field")
|
|
|
|
except json.JSONDecodeError as e:
|
|
result.issues.append(f"JSON parse error: {str(e)[:50]}")
|
|
else:
|
|
result.issues.append("No JSON object found in response")
|
|
|
|
return result
|
|
|
|
|
|
def _check_action_fields(action_type: str, parsed: dict) -> bool:
|
|
"""Check if required fields are present for action type"""
|
|
required_fields = {
|
|
"buy": ["market", "amount"],
|
|
"sell": ["market", "amount"],
|
|
"open_perp": ["ticker", "size", "direction"],
|
|
"close_perp": ["ticker", "size"],
|
|
"wait": [],
|
|
"trade": ["market"],
|
|
"predict": ["market"],
|
|
"post": ["content"],
|
|
"create_post": ["content"],
|
|
"send_dm": ["recipient"],
|
|
"dm": ["recipient"],
|
|
"research": [],
|
|
"analyze": [],
|
|
}
|
|
|
|
fields_needed = required_fields.get(action_type, [])
|
|
return all(field in parsed for field in fields_needed)
|
|
|
|
|
|
def analyze_reasoning_quality(thinking_content: str) -> ReasoningQualityResult:
|
|
"""
|
|
Analyze quality of reasoning in thinking content.
|
|
|
|
Checks for presence of analysis terms, justifications, and risk awareness.
|
|
"""
|
|
result = ReasoningQualityResult()
|
|
|
|
if not thinking_content:
|
|
return result
|
|
|
|
content_lower = thinking_content.lower()
|
|
|
|
# Count analysis terms
|
|
for term in ANALYSIS_TERMS:
|
|
if term in content_lower:
|
|
result.analysis_term_count += 1
|
|
|
|
# Check decision terms
|
|
for term in DECISION_TERMS:
|
|
if term in content_lower:
|
|
result.decision_term_count += 1
|
|
result.has_decision_justification = result.decision_term_count > 0
|
|
|
|
# Check risk terms
|
|
for term in RISK_TERMS:
|
|
if term in content_lower:
|
|
result.risk_term_count += 1
|
|
result.has_risk_consideration = result.risk_term_count > 0
|
|
|
|
# Count numerical references
|
|
numbers = NUMERICAL_PATTERN.findall(thinking_content)
|
|
result.numerical_count = len(numbers)
|
|
|
|
# Check for market-specific analysis
|
|
market_terms = {"btc", "eth", "bitcoin", "ethereum", "crypto", "stock", "market"}
|
|
result.has_market_analysis = any(term in content_lower for term in market_terms)
|
|
|
|
# Quality issues
|
|
if result.analysis_term_count < 2:
|
|
result.issues.append("Limited market analysis vocabulary")
|
|
|
|
if not result.has_decision_justification:
|
|
result.issues.append("No decision justification phrases")
|
|
|
|
return result
|
|
|
|
|
|
def analyze_length(
|
|
response: str,
|
|
thinking_content: str,
|
|
action_json: str,
|
|
) -> LengthAnalysisResult:
|
|
"""
|
|
Analyze response length characteristics.
|
|
"""
|
|
result = LengthAnalysisResult()
|
|
|
|
result.total_length = len(response)
|
|
result.thinking_length = len(thinking_content)
|
|
result.action_length = len(action_json)
|
|
|
|
# Check total length
|
|
result.is_too_short = result.total_length < MIN_RESPONSE_LENGTH
|
|
result.is_too_long = result.total_length > MAX_RESPONSE_LENGTH
|
|
|
|
# Check thinking length
|
|
result.thinking_is_too_short = result.thinking_length < MIN_THINKING_LENGTH
|
|
result.thinking_is_too_long = result.thinking_length > MAX_THINKING_LENGTH
|
|
|
|
return result
|
|
|
|
|
|
# =============================================================================
|
|
# Main Validation Function
|
|
# =============================================================================
|
|
|
|
|
|
def validate_response_format(response: str) -> FormatValidationResult:
|
|
"""
|
|
Validate complete response format.
|
|
|
|
Performs all validation checks and returns comprehensive result.
|
|
"""
|
|
# Validate think tags
|
|
think_result = validate_think_tags(response)
|
|
|
|
# Validate action JSON
|
|
action_result = validate_action_json(response)
|
|
|
|
# Analyze reasoning quality
|
|
reasoning_result = analyze_reasoning_quality(think_result.thinking_content)
|
|
|
|
# Analyze length
|
|
length_result = analyze_length(
|
|
response,
|
|
think_result.thinking_content,
|
|
action_result.raw_json,
|
|
)
|
|
|
|
return FormatValidationResult(
|
|
think_tags=think_result,
|
|
action=action_result,
|
|
reasoning=reasoning_result,
|
|
length=length_result,
|
|
)
|
|
|
|
|
|
def get_format_and_reasoning_scores(response: str) -> tuple[float, float]:
|
|
"""
|
|
Convenience function to get format and reasoning scores.
|
|
|
|
Returns:
|
|
(format_score, reasoning_score) both in range [0, 1]
|
|
"""
|
|
result = validate_response_format(response)
|
|
return result.format_score, result.reasoning_score
|
|
|
|
|
|
def validate_for_training(response: str) -> dict:
|
|
"""
|
|
Validate response format for training reward calculation.
|
|
|
|
Returns dict compatible with reward function inputs.
|
|
"""
|
|
result = validate_response_format(response)
|
|
summary = result.get_summary()
|
|
|
|
return {
|
|
"format_score": summary["format_score"],
|
|
"reasoning_score": summary["reasoning_score"],
|
|
"has_thinking": summary["has_thinking"],
|
|
"has_valid_action": summary["has_valid_action"],
|
|
"action_type": summary["action_type"],
|
|
"thinking_length": summary["thinking_length"],
|
|
"issues": summary["issues"],
|
|
}
|