Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:43:05 +08:00

625 lines
19 KiB
Python

"""
Shared Quality Utilities
Common quality scoring and validation functions used across the training pipeline.
Extracted to avoid duplication between rollout_generator and fast_simulator.
ENHANCED v3:
- Archetype-specific scoring weights
- Reasoning-action alignment validation with Financial Literacy
- XML Structure validation
- Coherence heuristics
- Curriculum learning support
"""
import re
from dataclasses import dataclass
from datetime import datetime, timezone
from typing import TYPE_CHECKING, Literal
from .models import (
Action,
FeedTrajectory,
EnvironmentState,
TrajectoryStep,
)
if TYPE_CHECKING:
from .rollout_generator import AgentTickData
# Archetype-specific quality weights
ARCHETYPE_WEIGHTS: dict[str, dict[str, float]] = {
# Research-heavy archetypes prioritize reasoning
"researcher": {"llm_calls": 0.3, "reasoning": 0.45, "action": 0.15, "feedback": 0.1},
"information-trader": {"llm_calls": 0.3, "reasoning": 0.4, "action": 0.2, "feedback": 0.1},
"super-predictor": {"llm_calls": 0.3, "reasoning": 0.4, "action": 0.2, "feedback": 0.1},
# Action-heavy archetypes prioritize execution
"trader": {"llm_calls": 0.3, "reasoning": 0.2, "action": 0.4, "feedback": 0.1},
"degen": {"llm_calls": 0.2, "reasoning": 0.15, "action": 0.55, "feedback": 0.1},
"perps-trader": {"llm_calls": 0.25, "reasoning": 0.2, "action": 0.45, "feedback": 0.1},
# Social archetypes prioritize engagement (response quality)
"social-butterfly": {"llm_calls": 0.35, "reasoning": 0.25, "action": 0.25, "feedback": 0.15},
"ass-kisser": {"llm_calls": 0.35, "reasoning": 0.3, "action": 0.2, "feedback": 0.15},
"goody-twoshoes": {"llm_calls": 0.35, "reasoning": 0.3, "action": 0.2, "feedback": 0.15},
# Deceptive archetypes prioritize reasoning (planning deception)
"scammer": {"llm_calls": 0.25, "reasoning": 0.4, "action": 0.25, "feedback": 0.1},
"liar": {"llm_calls": 0.25, "reasoning": 0.4, "action": 0.25, "feedback": 0.1},
# Balanced
"infosec": {"llm_calls": 0.3, "reasoning": 0.3, "action": 0.3, "feedback": 0.1},
# Default
"default": {"llm_calls": 0.4, "reasoning": 0.3, "action": 0.2, "feedback": 0.1},
}
def validate_xml_structure(response: str) -> float:
"""
Validate that the response contains valid decision XML tags.
Criteria:
1. Must contain <decisions> and </decisions> tags.
2. Must contain at least one <decision> tag.
3. Attributes 'amount' and 'ticker' (or 'marketId') should be present.
Returns:
+0.5 for valid syntax and attributes
-1.0 for broken XML or missing tags
-0.2 for missing attributes in otherwise valid tags
"""
if not response:
return -1.0
# Check for wrapping tags
if "<decisions>" not in response or "</decisions>" not in response:
return -1.0
# Check for inner tags
if "<decision" not in response:
return -0.5 # Has wrappers but no decision?
# Check for critical attributes (simple heuristic regex to handle both quote styles)
has_ticker = re.search(r'ticker="[^"]+"', response) or re.search(r"ticker='[^']+'", response)
has_market = re.search(r'marketId="[^"]+"', response) or re.search(
r"marketId='[^']+'", response
)
has_amount = re.search(r'amount="[^"]+"', response) or re.search(r"amount='[^']+'", response)
# Need either ticker OR marketId, AND amount
if (not has_ticker and not has_market) or not has_amount:
return -0.2 # Penalty for partial hallucination / missing args
return 0.5
def check_reasoning_action_alignment(
reasoning_text: str,
action: Action | None,
) -> float:
"""
Check if reasoning aligns with action taken, including Financial Literacy check.
Components:
1. Directional Alignment (Up/Buy vs Down/Sell)
2. Financial Literacy Bonus (referencing Exposure or PnL)
Returns:
Score between 0.0 and 1.0
"""
if not action or not reasoning_text:
return 0.5 # Neutral if we can't check
reasoning_lower = reasoning_text.lower()
action_type = action.action_type.lower()
score = 0.5
# --- 1. Financial Literacy Check ---
literacy_bonus = 0.0
if "exposure" in reasoning_lower:
literacy_bonus += 0.15
if "pnl" in reasoning_lower or "profit" in reasoning_lower or "loss" in reasoning_lower:
literacy_bonus += 0.15
# --- 2. Directional Alignment ---
# Sentiment indicators
bullish_words = ["bullish", "buy", "long", "upward", "positive", "opportunity", "moon"]
bearish_words = ["bearish", "sell", "short", "downward", "negative", "avoid", "dump"]
wait_words = ["wait", "hold", "unclear", "uncertain", "need more data", "observing"]
# Count sentiment
bullish_score = sum(1 for w in bullish_words if w in reasoning_lower)
bearish_score = sum(1 for w in bearish_words if w in reasoning_lower)
wait_score = sum(1 for w in wait_words if w in reasoning_lower)
# Check alignment
is_buy = action_type in ["buy", "buy_prediction", "open_perp", "long"]
is_sell = action_type in ["sell", "sell_prediction", "close_perp", "short"]
is_wait = action_type in ["wait", "hold"]
if is_buy:
if bullish_score > bearish_score:
score = 0.7 # Aligned
elif bearish_score > bullish_score:
score = 0.0 # Misaligned (Hallucination penalty)
else:
score = 0.4
elif is_sell:
if bearish_score > bullish_score:
score = 0.7 # Aligned
elif bullish_score > bearish_score:
score = 0.0 # Misaligned (Hallucination penalty)
else:
score = 0.4
elif is_wait:
if wait_score > 0:
score = 0.7
else:
score = 0.5
# Cap total at 1.0
return min(1.0, score + literacy_bonus)
def check_reasoning_coherence(reasoning_text: str) -> float:
"""
Check reasoning coherence using simple heuristics (0-1 score).
"""
if not reasoning_text or len(reasoning_text) < 20:
return 0.1
score = 0.0
text = reasoning_text
# Check for structure (numbered lists, bullet points)
if re.search(r"(\d+[\.\):]|\-|\*|\•)", text):
score += 0.25
# Check for conclusion markers
conclusion_markers = [
"therefore",
"conclusion",
"decision",
"recommend",
"suggest",
"final",
"result",
"action:",
"execute",
]
if any(marker in text.lower() for marker in conclusion_markers):
score += 0.25
# Check sentence count (2-10 sentences is ideal)
sentences = text.split(". ")
if 2 <= len(sentences) <= 10:
score += 0.2
elif len(sentences) > 10:
score += 0.1 # Too verbose
# Check for repetitive patterns (bad quality indicator)
words = text.lower().split()
if len(words) > 10:
unique_ratio = len(set(words)) / len(words)
if unique_ratio > 0.4:
score += 0.15 # Good vocabulary diversity
else:
score -= 0.1 # Repetitive
else:
score += 0.1
# Check for numeric analysis (prices, percentages)
if re.search(r"\$?\d+(?:\.\d+)?(?:%|k|K|M)?", text):
score += 0.15 # Contains quantitative analysis
return min(max(score, 0.0), 1.0)
def calculate_detailed_tick_quality(
llm_calls: list,
action: Action | None,
feedback: dict | None,
archetype: str | None = None,
) -> tuple[float, float]:
"""
Calculate detailed quality scores.
Returns: (format_score, reasoning_score)
"""
format_score = 0.0
reasoning_score = 0.0
# 1. Format Score (XML)
if llm_calls:
last_call = llm_calls[-1]
if last_call.response:
format_score = validate_xml_structure(last_call.response)
# 2. Reasoning Score
reasoning_texts = []
for call in llm_calls:
if call.reasoning:
reasoning_texts.append(call.reasoning)
if call.response:
reasoning_texts.append(call.response)
if action and action.reasoning:
reasoning_texts.append(action.reasoning)
full_reasoning = " ".join(reasoning_texts)
if full_reasoning:
reasoning_score = check_reasoning_action_alignment(full_reasoning, action)
# Coherence boost
reasoning_score += check_reasoning_coherence(full_reasoning) * 0.2
return format_score, min(1.0, reasoning_score)
def calculate_tick_quality_score(
llm_calls: list,
action: Action | None,
feedback: dict | None,
archetype: str | None = None,
) -> float:
"""
Calculate quality score for a single tick (0-1).
Legacy wrapper that returns a single float to maintain API compatibility.
"""
weights = ARCHETYPE_WEIGHTS.get(archetype or "default", ARCHETYPE_WEIGHTS["default"])
# Get detailed scores
fmt, rsn = calculate_detailed_tick_quality(llm_calls, action, feedback, archetype)
# Calculate action score separately as before
action_score = 0.0
if action:
if action.success:
action_score = 1.0
elif action.error:
action_score = 0.25
else:
action_score = 0.5
feedback_score = 0.0
if feedback:
feedback_score = 1.0
# Combine using legacy weights logic plus new components
# We map format (-1 to 0.5) to a 0-1 range for the legacy score roughly:
# 0.5 -> 1.0, -1.0 -> 0.0
normalized_format = (fmt + 1.0) / 1.5
total_score = (
normalized_format * weights["llm_calls"]
+ rsn * weights["reasoning"]
+ action_score * weights["action"]
+ feedback_score * weights["feedback"]
)
return min(1.0, max(0.0, total_score))
CurriculumLevel = Literal["easy", "medium", "hard"]
@dataclass
class TrajectoryDifficulty:
"""Trajectory difficulty assessment for curriculum learning"""
level: CurriculumLevel
score: float # 0-1, higher = harder
reasons: list[str]
def calculate_trajectory_quality_score(
ticks: list["AgentTickData"],
archetype: str | None = None,
) -> float:
"""
Calculate overall quality score for a trajectory (0-1).
Args:
ticks: List of tick data
archetype: Agent archetype for weight customization
"""
if not ticks:
return 0.0
scores = [
calculate_tick_quality_score(
tick.llm_calls,
tick.action,
tick.feedback,
archetype=archetype,
)
for tick in ticks
]
return sum(scores) / len(scores)
def assess_trajectory_difficulty(
ticks: list["AgentTickData"],
) -> TrajectoryDifficulty:
"""
Assess difficulty of a trajectory for curriculum learning.
Difficulty factors:
- Number of market changes
- Action complexity (leverage, size)
- Decision reversals
- Length of reasoning required
"""
reasons = []
difficulty_score = 0.0
if not ticks:
return TrajectoryDifficulty(level="easy", score=0.0, reasons=["Empty trajectory"])
# Factor 1: Trajectory length (longer = harder)
if len(ticks) > 20:
difficulty_score += 0.2
reasons.append(f"Long trajectory ({len(ticks)} ticks)")
elif len(ticks) > 10:
difficulty_score += 0.1
# Factor 2: Action diversity (more diverse = harder)
action_types = set()
for tick in ticks:
if tick.action:
action_types.add(tick.action.action_type)
if len(action_types) >= 4:
difficulty_score += 0.2
reasons.append(f"High action diversity ({len(action_types)} types)")
elif len(action_types) >= 2:
difficulty_score += 0.1
# Factor 3: Complex parameters (leverage, large sizes)
complex_actions = 0
for tick in ticks:
if tick.action and tick.action.parameters:
params = tick.action.parameters
# Explicitly cast to string then float to satisfy Pylance
try:
leverage = float(str(params.get("leverage", 1)))
if leverage > 1:
complex_actions += 1
except (ValueError, TypeError):
pass
try:
amount = float(str(params.get("amount", 0)))
if amount > 1000:
complex_actions += 1
except (ValueError, TypeError):
pass
if complex_actions >= 3:
difficulty_score += 0.2
reasons.append(f"Complex action parameters ({complex_actions})")
elif complex_actions >= 1:
difficulty_score += 0.1
# Factor 4: Decision reversals (buy -> sell in short time)
reversals = 0
prev_action = None
for tick in ticks:
if tick.action:
curr = tick.action.action_type
if prev_action:
if (prev_action in ["buy", "long"] and curr in ["sell", "short"]) or (
prev_action in ["sell", "short"] and curr in ["buy", "long"]
):
reversals += 1
prev_action = curr
if reversals >= 2:
difficulty_score += 0.2
reasons.append(f"Multiple reversals ({reversals})")
elif reversals >= 1:
difficulty_score += 0.1
# Factor 5: Reasoning depth required
total_reasoning_len = sum(
sum(len(c.reasoning or "") for c in tick.llm_calls)
+ len((tick.action.reasoning or "") if tick.action else "")
for tick in ticks
)
avg_reasoning = total_reasoning_len / len(ticks) if ticks else 0
if avg_reasoning > 200:
difficulty_score += 0.2
reasons.append(f"Deep reasoning required (avg {avg_reasoning:.0f} chars)")
elif avg_reasoning > 100:
difficulty_score += 0.1
# Normalize and categorize
difficulty_score = min(difficulty_score, 1.0)
if difficulty_score >= 0.6:
level: CurriculumLevel = "hard"
elif difficulty_score >= 0.3:
level = "medium"
else:
level = "easy"
return TrajectoryDifficulty(
level=level,
score=difficulty_score,
reasons=reasons if reasons else ["Standard complexity"],
)
def build_trajectory_from_ticks(
trajectory_id: str,
agent_id: str,
ticks: list["AgentTickData"],
min_steps: int = 1,
) -> FeedTrajectory | None:
"""
Build a FeedTrajectory from tick data.
Args:
trajectory_id: Unique trajectory ID
agent_id: Agent ID
ticks: List of AgentTickData
min_steps: Minimum steps required (returns None if fewer)
Returns:
FeedTrajectory or None if insufficient data
"""
if len(ticks) < min_steps:
return None
steps = []
for tick in ticks:
step = TrajectoryStep(
step_number=tick.tick_number,
timestamp=tick.timestamp,
environment_state=tick.environment_state,
provider_accesses=[],
llm_calls=tick.llm_calls,
action=tick.action
or Action(
action_type="wait",
parameters={},
success=True,
),
reward=tick.reward,
)
steps.append(step)
# Calculate final metrics
final_pnl = ticks[-1].environment_state.agent_pnl if ticks else 0.0
final_balance = ticks[-1].environment_state.agent_balance if ticks else 10000.0
total_reward = sum(t.reward for t in ticks)
# Count trades and posts
trades_executed = sum(
1
for t in ticks
if t.action
and t.action.action_type
in ["buy", "sell", "buy_prediction", "sell_prediction", "open_perp", "close_perp"]
)
posts_created = sum(
1 for t in ticks if t.action and t.action.action_type in ["create_post", "post"]
)
now = datetime.now(timezone.utc)
return FeedTrajectory(
id=trajectory_id,
trajectory_id=trajectory_id,
agent_id=agent_id,
window_id=now.strftime("%Y-%m-%dT%H:00"),
start_time=datetime.fromtimestamp(ticks[0].timestamp / 1000, tz=timezone.utc),
end_time=datetime.fromtimestamp(ticks[-1].timestamp / 1000, tz=timezone.utc),
duration_ms=ticks[-1].timestamp - ticks[0].timestamp,
steps=steps,
total_reward=total_reward,
final_pnl=final_pnl,
final_balance=final_balance,
trades_executed=trades_executed,
posts_created=posts_created,
episode_length=len(steps),
final_status="completed",
)
def state_to_observation(game_state: dict) -> dict:
"""Convert game state to agent observation"""
return {
"tick": game_state.get("tick", 0),
"time": game_state.get("currentTime", 0),
"markets": game_state.get("predictionMarkets", []),
"perpetuals": game_state.get("perpetualMarkets", []),
"news": game_state.get("news", [])[:5], # Limit for speed
"posts": game_state.get("socialFeed", [])[:10],
}
def state_to_env_state(game_state: dict, agent_id: str) -> EnvironmentState:
"""Extract environment state for an agent from game state"""
# Find agent's portfolio
portfolio = {}
for p in game_state.get("portfolios", []):
if p.get("agentId") == agent_id:
portfolio = p
break
return EnvironmentState(
agent_balance=portfolio.get("balance", 10000.0),
agentPnL=portfolio.get("pnl", 0.0),
open_positions=portfolio.get("positionCount", portfolio.get("positions", 0)),
active_markets=len(game_state.get("predictionMarkets", [])),
)
@dataclass
class ValidationResult:
"""Result of rollout validation"""
is_valid: bool
issues: list[str]
quality_score: float
@property
def issue_count(self) -> int:
return len(self.issues)
def validate_trajectory_quality(
ticks: list["AgentTickData"],
min_ticks: int = 5,
min_llm_calls_per_tick: float = 0.8, # 80% of ticks should have LLM calls
min_quality_score: float = 0.5,
) -> ValidationResult:
"""
Validate trajectory meets quality requirements for training.
Args:
ticks: List of tick data
min_ticks: Minimum number of ticks required
min_llm_calls_per_tick: Minimum fraction of ticks with LLM calls
min_quality_score: Minimum quality score threshold
Returns:
ValidationResult with validity, issues, and score
"""
issues: list[str] = []
# Check tick count
if len(ticks) < min_ticks:
issues.append(f"Too few ticks: {len(ticks)} < {min_ticks}")
if not ticks:
return ValidationResult(is_valid=False, issues=issues, quality_score=0.0)
# Check LLM call coverage
ticks_with_calls = sum(1 for t in ticks if t.llm_calls)
call_coverage = ticks_with_calls / len(ticks)
if call_coverage < min_llm_calls_per_tick:
issues.append(f"Low LLM call coverage: {call_coverage:.1%} < {min_llm_calls_per_tick:.1%}")
# Check for empty LLM calls
empty_calls = 0
for tick in ticks:
for call in tick.llm_calls:
if not call.user_prompt or not call.response:
empty_calls += 1
if empty_calls > 0:
issues.append(f"{empty_calls} LLM calls with empty prompt/response")
# Calculate quality score
quality_score = calculate_trajectory_quality_score(ticks)
if quality_score < min_quality_score:
issues.append(f"Quality score too low: {quality_score:.2f} < {min_quality_score}")
return ValidationResult(
is_valid=len(issues) == 0,
issues=issues,
quality_score=quality_score,
)