feat: Multi-user system with PostgreSQL - WIP temporary save
This commit is contained in:
@@ -97,6 +97,227 @@ class AgentCoordinator:
|
||||
self.neutral_analyst = NeutralAnalyst()
|
||||
self.safe_analyst = SafeAnalyst()
|
||||
|
||||
def run_analysis_stream(self, market: str, symbol: str, language: str = 'zh-CN', model: str = None, timeframe: str = "1D", on_progress=None):
|
||||
"""
|
||||
Run the full multi-agent analysis workflow with progress callbacks.
|
||||
|
||||
Args:
|
||||
on_progress: Callback function that receives (agent_name: str, status: str, result: Optional[dict])
|
||||
status can be: 'started', 'completed', 'error'
|
||||
|
||||
Yields:
|
||||
Progress events as dicts: { 'agent': str, 'status': str, 'result': dict or None }
|
||||
"""
|
||||
logger.info(f"Multi-agent stream analysis start: {market}:{symbol}, model={model}, language={language}")
|
||||
|
||||
def emit_progress(agent: str, status: str, result: dict = None):
|
||||
"""Emit progress event."""
|
||||
if on_progress:
|
||||
on_progress(agent, status, result)
|
||||
|
||||
# Build base context
|
||||
from .tools import AgentTools
|
||||
tools = AgentTools()
|
||||
|
||||
emit_progress('data_collection', 'started', None)
|
||||
|
||||
# 1) Base data
|
||||
current_price = tools.get_current_price(market, symbol)
|
||||
company_data = tools.get_company_data(market, symbol, language=language)
|
||||
|
||||
# Normalize timeframe
|
||||
tf = (timeframe or "1D").strip()
|
||||
|
||||
# 2) Kline + fundamentals
|
||||
kline_data = tools.get_stock_data(market, symbol, days=30, timeframe=tf)
|
||||
fundamental_data = tools.get_fundamental_data(market, symbol)
|
||||
indicators = tools.calculate_technical_indicators(kline_data or [])
|
||||
|
||||
# 3) News (Finnhub + web search)
|
||||
company_name = company_data.get('name', symbol) if company_data else symbol
|
||||
news_data = tools.get_news(market, symbol, days=7, company_name=company_name)
|
||||
|
||||
base_data = {
|
||||
"market": market,
|
||||
"symbol": symbol,
|
||||
"current_price": current_price,
|
||||
"kline_data": kline_data,
|
||||
"fundamental_data": fundamental_data,
|
||||
"company_data": company_data,
|
||||
"news_data": news_data,
|
||||
"indicators": indicators,
|
||||
}
|
||||
|
||||
context = {
|
||||
"market": market,
|
||||
"symbol": symbol,
|
||||
"language": language,
|
||||
"model": model,
|
||||
"timeframe": tf,
|
||||
"memory_features": {
|
||||
"timeframe": tf,
|
||||
"price": (current_price or {}).get("price"),
|
||||
"changePercent": (current_price or {}).get("changePercent"),
|
||||
"indicators": indicators,
|
||||
},
|
||||
"base_data": base_data
|
||||
}
|
||||
|
||||
emit_progress('data_collection', 'completed', None)
|
||||
|
||||
# Phase 1: Analysts (parallel but report individually)
|
||||
logger.info("Phase 1: Analyst team")
|
||||
|
||||
# Fundamental Analyst
|
||||
emit_progress('fundamental', 'started', None)
|
||||
fundamental_report = self.fundamental_analyst.analyze(context)
|
||||
emit_progress('fundamental', 'completed', fundamental_report.get('data', {}))
|
||||
|
||||
# Technical Analyst (market_analyst)
|
||||
emit_progress('technical', 'started', None)
|
||||
market_report = self.market_analyst.analyze(context)
|
||||
emit_progress('technical', 'completed', market_report.get('data', {}))
|
||||
|
||||
# News Analyst
|
||||
emit_progress('news', 'started', None)
|
||||
news_report = self.news_analyst.analyze(context)
|
||||
emit_progress('news', 'completed', news_report.get('data', {}))
|
||||
|
||||
# Sentiment Analyst
|
||||
emit_progress('sentiment', 'started', None)
|
||||
sentiment_report = self.sentiment_analyst.analyze(context)
|
||||
emit_progress('sentiment', 'completed', sentiment_report.get('data', {}))
|
||||
|
||||
# Risk Analyst
|
||||
emit_progress('risk', 'started', None)
|
||||
risk_report = self.risk_analyst.analyze(context)
|
||||
emit_progress('risk', 'completed', risk_report.get('data', {}))
|
||||
|
||||
# Update context with analyst outputs
|
||||
context.update({
|
||||
"market_report": market_report,
|
||||
"fundamental_report": fundamental_report,
|
||||
"news_report": news_report,
|
||||
"sentiment_report": sentiment_report,
|
||||
"risk_report": risk_report,
|
||||
})
|
||||
|
||||
# Phase 2: Bull/Bear debate
|
||||
logger.info("Phase 2: Research debate")
|
||||
|
||||
emit_progress('debate_bull', 'started', None)
|
||||
bull_argument = self.bull_researcher.analyze(context)
|
||||
emit_progress('debate_bull', 'completed', bull_argument.get('data', {}))
|
||||
|
||||
emit_progress('debate_bear', 'started', None)
|
||||
bear_argument = self.bear_researcher.analyze(context)
|
||||
emit_progress('debate_bear', 'completed', bear_argument.get('data', {}))
|
||||
|
||||
context["bull_argument"] = bull_argument
|
||||
context["bear_argument"] = bear_argument
|
||||
|
||||
# Research manager decision
|
||||
emit_progress('debate_research', 'started', None)
|
||||
research_decision = self._make_research_decision(bull_argument, bear_argument, context)
|
||||
context["research_decision"] = research_decision
|
||||
emit_progress('debate_research', 'completed', {'research_decision': research_decision})
|
||||
|
||||
# Phase 3: Trader decision
|
||||
logger.info("Phase 3: Trader decision")
|
||||
emit_progress('trader_decision', 'started', None)
|
||||
trader_result = self.trader_agent.analyze(context)
|
||||
trader_plan = trader_result.get('data', {}).get('trading_plan', {})
|
||||
context["trader_plan"] = trader_plan
|
||||
emit_progress('trader_decision', 'completed', trader_result.get('data', {}))
|
||||
|
||||
# Phase 4: Risk debate
|
||||
logger.info("Phase 4: Risk debate")
|
||||
|
||||
emit_progress('risk_debate_risky', 'started', None)
|
||||
risky_result = self.risky_analyst.analyze(context)
|
||||
emit_progress('risk_debate_risky', 'completed', risky_result.get('data', {}))
|
||||
|
||||
emit_progress('risk_debate_neutral', 'started', None)
|
||||
neutral_result = self.neutral_analyst.analyze(context)
|
||||
emit_progress('risk_debate_neutral', 'completed', neutral_result.get('data', {}))
|
||||
|
||||
emit_progress('risk_debate_safe', 'started', None)
|
||||
safe_result = self.safe_analyst.analyze(context)
|
||||
emit_progress('risk_debate_safe', 'completed', safe_result.get('data', {}))
|
||||
|
||||
# Final decision
|
||||
logger.info("Phase 5: Final decision")
|
||||
emit_progress('final_decision', 'started', None)
|
||||
final_decision = self._make_risk_decision(risky_result, neutral_result, safe_result, trader_result, context)
|
||||
emit_progress('final_decision', 'completed', final_decision)
|
||||
|
||||
# Generate overview
|
||||
emit_progress('overview', 'started', None)
|
||||
overview = self._generate_overview(context, final_decision)
|
||||
emit_progress('overview', 'completed', overview)
|
||||
|
||||
# Record for reflection
|
||||
if self.reflection_service and final_decision.get('decision') in ['BUY', 'SELL', 'HOLD']:
|
||||
try:
|
||||
self.reflection_service.record_analysis(
|
||||
market=market,
|
||||
symbol=symbol,
|
||||
price=base_data.get('current_price', {}).get('price'),
|
||||
decision=final_decision.get('decision'),
|
||||
confidence=final_decision.get('confidence', 50),
|
||||
reasoning=final_decision.get('reasoning', ''),
|
||||
check_days=7
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"Record reflection failed: {e}")
|
||||
|
||||
# Build final result
|
||||
debate_data = {
|
||||
"bull": bull_argument.get('data', {}) if bull_argument.get('data') else {},
|
||||
"bear": bear_argument.get('data', {}) if bear_argument.get('data') else {},
|
||||
"research_decision": research_decision if research_decision else "Analyzing..."
|
||||
}
|
||||
|
||||
trader_decision_data = trader_result.get('data', {}) if trader_result.get('data') else {
|
||||
"decision": "HOLD",
|
||||
"confidence": 50,
|
||||
"reasoning": "Analyzing...",
|
||||
"trading_plan": {},
|
||||
"report": "Analyzing..."
|
||||
}
|
||||
|
||||
risk_debate_data = {
|
||||
"risky": risky_result.get('data', {}) if risky_result.get('data') else {},
|
||||
"neutral": neutral_result.get('data', {}) if neutral_result.get('data') else {},
|
||||
"safe": safe_result.get('data', {}) if safe_result.get('data') else {}
|
||||
}
|
||||
|
||||
if not final_decision or (isinstance(final_decision, dict) and len(final_decision) == 0):
|
||||
final_decision = {
|
||||
"decision": "HOLD",
|
||||
"confidence": 50,
|
||||
"reasoning": "Analyzing...",
|
||||
"risk_summary": {},
|
||||
"recommendation": "Analyzing..."
|
||||
}
|
||||
|
||||
result = {
|
||||
"overview": overview,
|
||||
"fundamental": fundamental_report.get('data', {}),
|
||||
"technical": market_report.get('data', {}),
|
||||
"news": news_report.get('data', {}),
|
||||
"sentiment": sentiment_report.get('data', {}),
|
||||
"risk": risk_report.get('data', {}),
|
||||
"debate": debate_data,
|
||||
"trader_decision": trader_decision_data,
|
||||
"risk_debate": risk_debate_data,
|
||||
"final_decision": final_decision,
|
||||
"error": None
|
||||
}
|
||||
|
||||
logger.info(f"Multi-agent stream analysis completed: {market}:{symbol}")
|
||||
return result
|
||||
|
||||
def run_analysis(self, market: str, symbol: str, language: str = 'zh-CN', model: str = None, timeframe: str = "1D") -> Dict[str, Any]:
|
||||
"""
|
||||
Run the full multi-agent analysis workflow.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Agent memory system (local-only).
|
||||
Agent memory system (PostgreSQL).
|
||||
|
||||
This module stores agent experiences in SQLite and retrieves relevant past cases
|
||||
This module stores agent experiences in PostgreSQL and retrieves relevant past cases
|
||||
to inject into prompts (RAG-style). It does NOT finetune model weights.
|
||||
|
||||
Retrieval (configurable):
|
||||
@@ -14,7 +14,6 @@ Ranking combines:
|
||||
- optional returns weight
|
||||
"""
|
||||
|
||||
import sqlite3
|
||||
import json
|
||||
import os
|
||||
import math
|
||||
@@ -23,31 +22,24 @@ from datetime import datetime, timezone
|
||||
import difflib
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
from app.utils.db import get_db_connection
|
||||
from .embedding import EmbeddingService, cosine_sim
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AgentMemory:
|
||||
"""智能体记忆系统"""
|
||||
"""Agent memory system using PostgreSQL"""
|
||||
|
||||
def __init__(self, agent_name: str, db_path: Optional[str] = None):
|
||||
"""
|
||||
初始化记忆系统
|
||||
Initialize memory system.
|
||||
|
||||
Args:
|
||||
agent_name: 智能体名称
|
||||
db_path: 数据库路径(可选)
|
||||
agent_name: Agent identifier (e.g., 'trader_agent', 'risk_analyst')
|
||||
db_path: Deprecated parameter, kept for backward compatibility
|
||||
"""
|
||||
self.agent_name = agent_name
|
||||
|
||||
if db_path is None:
|
||||
# 默认数据库路径
|
||||
db_dir = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'data', 'memory')
|
||||
os.makedirs(db_dir, exist_ok=True)
|
||||
db_path = os.path.join(db_dir, f'{agent_name}_memory.db')
|
||||
|
||||
self.db_path = db_path
|
||||
self.embedder = EmbeddingService()
|
||||
self.enable_vector = os.getenv("AGENT_MEMORY_ENABLE_VECTOR", "true").lower() == "true"
|
||||
self.candidate_limit = int(os.getenv("AGENT_MEMORY_CANDIDATE_LIMIT", "500") or 500)
|
||||
@@ -55,58 +47,6 @@ class AgentMemory:
|
||||
self.w_sim = float(os.getenv("AGENT_MEMORY_W_SIM", "0.75") or 0.75)
|
||||
self.w_recency = float(os.getenv("AGENT_MEMORY_W_RECENCY", "0.20") or 0.20)
|
||||
self.w_returns = float(os.getenv("AGENT_MEMORY_W_RETURNS", "0.05") or 0.05)
|
||||
self._init_database()
|
||||
|
||||
def _init_database(self):
|
||||
"""初始化数据库表"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS memories (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
situation TEXT NOT NULL,
|
||||
recommendation TEXT NOT NULL,
|
||||
result TEXT,
|
||||
returns REAL,
|
||||
market TEXT,
|
||||
symbol TEXT,
|
||||
timeframe TEXT,
|
||||
features_json TEXT,
|
||||
embedding BLOB,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
)
|
||||
''')
|
||||
|
||||
# Best-effort migration for older DBs
|
||||
# NOTE: For existing tables, we must add missing columns BEFORE creating indexes
|
||||
# that reference them (otherwise we'll hit: "no such column: market").
|
||||
cursor.execute("PRAGMA table_info(memories)")
|
||||
existing_cols = {row[1] for row in cursor.fetchall() or []}
|
||||
for col, ddl in {
|
||||
"market": "TEXT",
|
||||
"symbol": "TEXT",
|
||||
"timeframe": "TEXT",
|
||||
"features_json": "TEXT",
|
||||
"embedding": "BLOB",
|
||||
}.items():
|
||||
if col not in existing_cols:
|
||||
cursor.execute(f"ALTER TABLE memories ADD COLUMN {col} {ddl}")
|
||||
|
||||
# 创建索引(放在迁移之后,兼容旧库)
|
||||
cursor.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_created_at ON memories(created_at)
|
||||
''')
|
||||
cursor.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_market_symbol ON memories(market, symbol)
|
||||
''')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
logger.error(f"初始化记忆数据库失败: {e}")
|
||||
|
||||
def _now_utc(self) -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
@@ -156,13 +96,13 @@ class AgentMemory:
|
||||
metadata: Optional[Dict[str, Any]] = None,
|
||||
):
|
||||
"""
|
||||
添加记忆
|
||||
Add a memory entry.
|
||||
|
||||
Args:
|
||||
situation: 情况描述
|
||||
recommendation: 建议/决策
|
||||
result: 结果描述(可选)
|
||||
returns: 收益(可选)
|
||||
situation: Situation description
|
||||
recommendation: Decision/recommendation made
|
||||
result: Outcome description (optional)
|
||||
returns: Return percentage (optional)
|
||||
metadata: Optional structured metadata (market/symbol/timeframe/features...)
|
||||
"""
|
||||
try:
|
||||
@@ -182,45 +122,50 @@ class AgentMemory:
|
||||
vec = self.embedder.embed(text)
|
||||
embedding_blob = self.embedder.to_bytes(vec)
|
||||
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO memories (situation, recommendation, result, returns, market, symbol, timeframe, features_json, embedding)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
''', (situation, recommendation, result, returns, market, symbol, timeframe, features_json, embedding_blob))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
logger.info(f"{self.agent_name} 添加新记忆")
|
||||
with get_db_connection() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO qd_agent_memories
|
||||
(agent_name, situation, recommendation, result, returns, market, symbol, timeframe, features_json, embedding)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(self.agent_name, situation, recommendation, result, returns, market, symbol, timeframe, features_json, embedding_blob)
|
||||
)
|
||||
conn.commit()
|
||||
cur.close()
|
||||
logger.info(f"{self.agent_name} added new memory")
|
||||
except Exception as e:
|
||||
logger.error(f"添加记忆失败: {e}")
|
||||
logger.error(f"Failed to add memory: {e}")
|
||||
|
||||
def get_memories(self, current_situation: str, n_matches: int = 5, metadata: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
检索相似记忆
|
||||
Retrieve similar memories.
|
||||
|
||||
Args:
|
||||
current_situation: 当前情况描述
|
||||
n_matches: 返回的匹配数量
|
||||
current_situation: Current situation description
|
||||
n_matches: Number of matches to return
|
||||
metadata: Optional metadata for filtering/weighting
|
||||
|
||||
Returns:
|
||||
匹配的记忆列表
|
||||
List of matching memory entries
|
||||
"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 获取所有记忆
|
||||
cursor.execute('''
|
||||
SELECT id, situation, recommendation, result, returns, created_at, market, symbol, timeframe, features_json, embedding
|
||||
FROM memories
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
''', (int(self.candidate_limit),))
|
||||
|
||||
all_memories = cursor.fetchall()
|
||||
conn.close()
|
||||
with get_db_connection() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, situation, recommendation, result, returns, created_at,
|
||||
market, symbol, timeframe, features_json, embedding
|
||||
FROM qd_agent_memories
|
||||
WHERE agent_name = ?
|
||||
ORDER BY created_at DESC
|
||||
LIMIT ?
|
||||
""",
|
||||
(self.agent_name, int(self.candidate_limit))
|
||||
)
|
||||
all_memories = cur.fetchall() or []
|
||||
cur.close()
|
||||
|
||||
if not all_memories:
|
||||
return []
|
||||
@@ -240,23 +185,24 @@ class AgentMemory:
|
||||
|
||||
ranked = []
|
||||
for row in all_memories:
|
||||
(
|
||||
mem_id,
|
||||
situation,
|
||||
recommendation,
|
||||
result,
|
||||
returns,
|
||||
created_at,
|
||||
market,
|
||||
symbol,
|
||||
timeframe,
|
||||
features_json,
|
||||
embedding_blob,
|
||||
) = row
|
||||
mem_id = row['id']
|
||||
situation = row['situation']
|
||||
recommendation = row['recommendation']
|
||||
result = row['result']
|
||||
returns = row['returns']
|
||||
created_at = row['created_at']
|
||||
market = row['market']
|
||||
symbol = row['symbol']
|
||||
timeframe = row['timeframe']
|
||||
features_json = row['features_json']
|
||||
embedding_blob = row['embedding']
|
||||
|
||||
sim = 0.0
|
||||
if self.enable_vector and embedding_blob:
|
||||
try:
|
||||
# Handle memoryview/bytes from PostgreSQL
|
||||
if isinstance(embedding_blob, memoryview):
|
||||
embedding_blob = bytes(embedding_blob)
|
||||
mem_vec = self.embedder.from_bytes(embedding_blob)
|
||||
sim = cosine_sim(query_vec, mem_vec)
|
||||
except Exception:
|
||||
@@ -292,50 +238,60 @@ class AgentMemory:
|
||||
return ranked[: max(0, int(n_matches or 0))]
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"检索记忆失败: {e}")
|
||||
logger.error(f"Failed to retrieve memories: {e}")
|
||||
return []
|
||||
|
||||
def update_memory_result(self, memory_id: int, result: str, returns: Optional[float] = None):
|
||||
"""
|
||||
更新记忆的结果
|
||||
Update memory result.
|
||||
|
||||
Args:
|
||||
memory_id: 记忆ID
|
||||
result: 结果描述
|
||||
returns: 收益
|
||||
memory_id: Memory ID
|
||||
result: Outcome description
|
||||
returns: Return percentage
|
||||
"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('''
|
||||
UPDATE memories
|
||||
SET result = ?, returns = ?, updated_at = CURRENT_TIMESTAMP
|
||||
WHERE id = ?
|
||||
''', (result, returns, memory_id))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
logger.info(f"{self.agent_name} 更新记忆 {memory_id}")
|
||||
with get_db_connection() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE qd_agent_memories
|
||||
SET result = ?, returns = ?, updated_at = NOW()
|
||||
WHERE id = ? AND agent_name = ?
|
||||
""",
|
||||
(result, returns, memory_id, self.agent_name)
|
||||
)
|
||||
conn.commit()
|
||||
cur.close()
|
||||
logger.info(f"{self.agent_name} updated memory {memory_id}")
|
||||
except Exception as e:
|
||||
logger.error(f"更新记忆失败: {e}")
|
||||
logger.error(f"Failed to update memory: {e}")
|
||||
|
||||
def get_statistics(self) -> Dict[str, Any]:
|
||||
"""获取记忆统计信息"""
|
||||
"""Get memory statistics for this agent."""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute('SELECT COUNT(*) FROM memories')
|
||||
total = cursor.fetchone()[0]
|
||||
|
||||
cursor.execute('SELECT AVG(returns) FROM memories WHERE returns IS NOT NULL')
|
||||
avg_returns = cursor.fetchone()[0] or 0
|
||||
|
||||
cursor.execute('SELECT COUNT(*) FROM memories WHERE returns > 0')
|
||||
positive = cursor.fetchone()[0]
|
||||
|
||||
conn.close()
|
||||
with get_db_connection() as conn:
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute(
|
||||
'SELECT COUNT(*) as cnt FROM qd_agent_memories WHERE agent_name = ?',
|
||||
(self.agent_name,)
|
||||
)
|
||||
total = cur.fetchone()['cnt']
|
||||
|
||||
cur.execute(
|
||||
'SELECT AVG(returns) as avg_ret FROM qd_agent_memories WHERE agent_name = ? AND returns IS NOT NULL',
|
||||
(self.agent_name,)
|
||||
)
|
||||
avg_returns = cur.fetchone()['avg_ret'] or 0
|
||||
|
||||
cur.execute(
|
||||
'SELECT COUNT(*) as cnt FROM qd_agent_memories WHERE agent_name = ? AND returns > 0',
|
||||
(self.agent_name,)
|
||||
)
|
||||
positive = cur.fetchone()['cnt']
|
||||
|
||||
cur.close()
|
||||
|
||||
return {
|
||||
'total_memories': total,
|
||||
@@ -344,5 +300,20 @@ class AgentMemory:
|
||||
'success_rate': round(positive / total * 100, 2) if total > 0 else 0
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"获取统计信息失败: {e}")
|
||||
logger.error(f"Failed to get statistics: {e}")
|
||||
return {}
|
||||
|
||||
def clear_memories(self):
|
||||
"""Clear all memories for this agent (use with caution)."""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
'DELETE FROM qd_agent_memories WHERE agent_name = ?',
|
||||
(self.agent_name,)
|
||||
)
|
||||
conn.commit()
|
||||
cur.close()
|
||||
logger.warning(f"{self.agent_name} cleared all memories")
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to clear memories: {e}")
|
||||
|
||||
@@ -1,220 +1,249 @@
|
||||
"""
|
||||
自动反思与验证服务
|
||||
用于记录分析预测,并在未来自动验证结果,实现闭环学习
|
||||
Auto-reflection and verification service (PostgreSQL).
|
||||
|
||||
Records analysis predictions and auto-verifies results in the future
|
||||
to achieve closed-loop learning for AI agents.
|
||||
"""
|
||||
import sqlite3
|
||||
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Dict, Any, Optional
|
||||
|
||||
from app.utils.logger import get_logger
|
||||
from app.utils.db import get_db_connection
|
||||
from .memory import AgentMemory
|
||||
from .tools import AgentTools
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ReflectionService:
|
||||
"""反思服务:管理分析记录的存储和验证"""
|
||||
"""Reflection service: manages storage and verification of analysis records."""
|
||||
|
||||
def __init__(self, db_path: Optional[str] = None):
|
||||
if db_path is None:
|
||||
# 默认数据库路径
|
||||
db_dir = os.path.join(os.path.dirname(__file__), '..', '..', '..', 'data', 'memory')
|
||||
os.makedirs(db_dir, exist_ok=True)
|
||||
db_path = os.path.join(db_dir, 'reflection_records.db')
|
||||
|
||||
self.db_path = db_path
|
||||
self.tools = AgentTools()
|
||||
self._init_database()
|
||||
|
||||
def _init_database(self):
|
||||
"""初始化数据库表"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 创建分析记录表
|
||||
cursor.execute('''
|
||||
CREATE TABLE IF NOT EXISTS analysis_records (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
market TEXT NOT NULL,
|
||||
symbol TEXT NOT NULL,
|
||||
initial_price REAL,
|
||||
decision TEXT,
|
||||
confidence INTEGER,
|
||||
reasoning TEXT,
|
||||
analysis_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
target_check_date TIMESTAMP,
|
||||
status TEXT DEFAULT 'PENDING', -- PENDING, COMPLETED, FAILED
|
||||
final_price REAL,
|
||||
actual_return REAL,
|
||||
check_result TEXT
|
||||
)
|
||||
''')
|
||||
|
||||
# 创建索引
|
||||
cursor.execute('''
|
||||
CREATE INDEX IF NOT EXISTS idx_status_date ON analysis_records(status, target_check_date)
|
||||
''')
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
except Exception as e:
|
||||
logger.error(f"初始化反思数据库失败: {e}")
|
||||
|
||||
def record_analysis(self, market: str, symbol: str, price: float,
|
||||
decision: str, confidence: int, reasoning: str,
|
||||
check_days: int = 7):
|
||||
"""
|
||||
记录一次分析,以便未来验证
|
||||
Initialize reflection service.
|
||||
|
||||
Args:
|
||||
market: 市场
|
||||
symbol: 代码
|
||||
price: 当前价格
|
||||
decision: 决策 (BUY/SELL/HOLD)
|
||||
confidence: 置信度
|
||||
reasoning: 理由
|
||||
check_days: 几天后验证 (默认7天)
|
||||
db_path: Deprecated parameter, kept for backward compatibility
|
||||
"""
|
||||
self.tools = AgentTools()
|
||||
|
||||
def record_analysis(
|
||||
self,
|
||||
market: str,
|
||||
symbol: str,
|
||||
price: float,
|
||||
decision: str,
|
||||
confidence: int,
|
||||
reasoning: str,
|
||||
check_days: int = 7
|
||||
):
|
||||
"""
|
||||
Record an analysis for future verification.
|
||||
|
||||
Args:
|
||||
market: Market type
|
||||
symbol: Symbol code
|
||||
price: Current price
|
||||
decision: Decision (BUY/SELL/HOLD)
|
||||
confidence: Confidence level (0-100)
|
||||
reasoning: Reasoning text
|
||||
check_days: Days until verification (default 7)
|
||||
"""
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
target_date = datetime.now() + timedelta(days=check_days)
|
||||
|
||||
cursor.execute('''
|
||||
INSERT INTO analysis_records
|
||||
(market, symbol, initial_price, decision, confidence, reasoning, target_check_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
''', (market, symbol, price, decision, confidence, reasoning, target_date))
|
||||
|
||||
conn.commit()
|
||||
conn.close()
|
||||
with get_db_connection() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO qd_reflection_records
|
||||
(market, symbol, initial_price, decision, confidence, reasoning, target_check_date)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(market, symbol, price, decision, confidence, reasoning, target_date)
|
||||
)
|
||||
conn.commit()
|
||||
cur.close()
|
||||
logger.info(f"Recorded analysis for reflection: {market}:{symbol}, will verify after {check_days} day(s)")
|
||||
except Exception as e:
|
||||
logger.error(f"记录分析失败: {e}")
|
||||
logger.error(f"Failed to record analysis: {e}")
|
||||
|
||||
def run_verification_cycle(self):
|
||||
"""
|
||||
执行验证周期:检查到期的记录,验证结果,并写入记忆
|
||||
Execute verification cycle: check due records, verify results, and write to memory.
|
||||
"""
|
||||
logger.info("开始执行自动反思验证周期...")
|
||||
logger.info("Starting auto-reflection verification cycle...")
|
||||
|
||||
try:
|
||||
conn = sqlite3.connect(self.db_path)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 1. 查找所有已到期且未处理的记录
|
||||
cursor.execute('''
|
||||
SELECT id, market, symbol, initial_price, decision, confidence, reasoning, analysis_date
|
||||
FROM analysis_records
|
||||
WHERE status = 'PENDING' AND target_check_date <= CURRENT_TIMESTAMP
|
||||
''')
|
||||
|
||||
records = cursor.fetchall()
|
||||
|
||||
if not records:
|
||||
logger.info("没有需要验证的记录")
|
||||
conn.close()
|
||||
return
|
||||
|
||||
logger.info(f"发现 {len(records)} 条待验证记录")
|
||||
|
||||
# 初始化记忆系统(用于写入验证结果)
|
||||
trader_memory = AgentMemory('trader_agent')
|
||||
|
||||
for record in records:
|
||||
record_id, market, symbol, initial_price, decision, confidence, reasoning, analysis_date = record
|
||||
with get_db_connection() as conn:
|
||||
cur = conn.cursor()
|
||||
|
||||
try:
|
||||
# 2. 获取当前最新价格
|
||||
current_price_data = self.tools.get_current_price(market, symbol)
|
||||
current_price = current_price_data.get('price')
|
||||
# 1. Find all due and pending records
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, market, symbol, initial_price, decision, confidence, reasoning, analysis_date
|
||||
FROM qd_reflection_records
|
||||
WHERE status = 'PENDING' AND target_check_date <= NOW()
|
||||
"""
|
||||
)
|
||||
records = cur.fetchall() or []
|
||||
|
||||
if not records:
|
||||
logger.info("No records to verify")
|
||||
cur.close()
|
||||
return
|
||||
|
||||
logger.info(f"Found {len(records)} records to verify")
|
||||
|
||||
# Initialize memory system for writing verification results
|
||||
trader_memory = AgentMemory('trader_agent')
|
||||
|
||||
for record in records:
|
||||
record_id = record['id']
|
||||
market = record['market']
|
||||
symbol = record['symbol']
|
||||
initial_price = record['initial_price']
|
||||
decision = record['decision']
|
||||
confidence = record['confidence']
|
||||
reasoning = record['reasoning']
|
||||
analysis_date = record['analysis_date']
|
||||
|
||||
if not current_price:
|
||||
logger.warning(f"无法获取 {market}:{symbol} 的当前价格,跳过")
|
||||
continue
|
||||
try:
|
||||
# 2. Get current price
|
||||
current_price_data = self.tools.get_current_price(market, symbol)
|
||||
current_price = current_price_data.get('price')
|
||||
|
||||
# 3. 计算收益和结果
|
||||
if not initial_price or initial_price == 0:
|
||||
actual_return = 0.0
|
||||
else:
|
||||
actual_return = (current_price - initial_price) / initial_price * 100
|
||||
|
||||
# 评估结果
|
||||
result_desc = ""
|
||||
is_good_prediction = False
|
||||
|
||||
if decision == "BUY":
|
||||
if actual_return > 2.0:
|
||||
result_desc = "Correct: price rose after BUY"
|
||||
is_good_prediction = True
|
||||
elif actual_return < -2.0:
|
||||
result_desc = "Wrong: price fell after BUY"
|
||||
if not current_price:
|
||||
logger.warning(f"Cannot get current price for {market}:{symbol}, skipping")
|
||||
continue
|
||||
|
||||
# 3. Calculate return and result
|
||||
if not initial_price or initial_price == 0:
|
||||
actual_return = 0.0
|
||||
else:
|
||||
result_desc = "Neutral: limited price movement"
|
||||
elif decision == "SELL":
|
||||
if actual_return < -2.0:
|
||||
result_desc = "Correct: price fell after SELL"
|
||||
is_good_prediction = True
|
||||
elif actual_return > 2.0:
|
||||
result_desc = "Wrong: price rose after SELL"
|
||||
else:
|
||||
result_desc = "Neutral: limited price movement"
|
||||
else: # HOLD
|
||||
if -2.0 <= actual_return <= 2.0:
|
||||
result_desc = "Correct: limited movement during HOLD"
|
||||
is_good_prediction = True
|
||||
else:
|
||||
result_desc = f"Deviated: large movement during HOLD ({actual_return:.2f}%)"
|
||||
actual_return = (current_price - initial_price) / initial_price * 100
|
||||
|
||||
# Evaluate result
|
||||
result_desc = ""
|
||||
is_good_prediction = False
|
||||
|
||||
if decision == "BUY":
|
||||
if actual_return > 2.0:
|
||||
result_desc = "Correct: price rose after BUY"
|
||||
is_good_prediction = True
|
||||
elif actual_return < -2.0:
|
||||
result_desc = "Wrong: price fell after BUY"
|
||||
else:
|
||||
result_desc = "Neutral: limited price movement"
|
||||
elif decision == "SELL":
|
||||
if actual_return < -2.0:
|
||||
result_desc = "Correct: price fell after SELL"
|
||||
is_good_prediction = True
|
||||
elif actual_return > 2.0:
|
||||
result_desc = "Wrong: price rose after SELL"
|
||||
else:
|
||||
result_desc = "Neutral: limited price movement"
|
||||
else: # HOLD
|
||||
if -2.0 <= actual_return <= 2.0:
|
||||
result_desc = "Correct: limited movement during HOLD"
|
||||
is_good_prediction = True
|
||||
else:
|
||||
result_desc = f"Deviated: large movement during HOLD ({actual_return:.2f}%)"
|
||||
|
||||
# 4. 写入记忆系统 (Let the agent learn)
|
||||
memory_situation = f"{market}:{symbol} auto-verified (analysis_date: {analysis_date})"
|
||||
memory_recommendation = f"Decision: {decision} (confidence {confidence}), reasoning: {(reasoning or '')[:120]}"
|
||||
memory_result = f"Verification: {result_desc}; return={actual_return:.2f}% (initial {initial_price} -> final {current_price})"
|
||||
|
||||
trader_memory.add_memory(
|
||||
memory_situation,
|
||||
memory_recommendation,
|
||||
memory_result,
|
||||
actual_return,
|
||||
metadata={
|
||||
"market": market,
|
||||
"symbol": symbol,
|
||||
"timeframe": "1D",
|
||||
"features": {
|
||||
"source": "auto_verify",
|
||||
"decision": decision,
|
||||
"confidence": confidence,
|
||||
"initial_price": initial_price,
|
||||
"final_price": current_price,
|
||||
"analysis_date": str(analysis_date),
|
||||
"result_desc": result_desc,
|
||||
"is_good_prediction": bool(is_good_prediction),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# 5. 更新记录状态
|
||||
cursor.execute('''
|
||||
UPDATE analysis_records
|
||||
SET status = 'COMPLETED', final_price = ?, actual_return = ?, check_result = ?
|
||||
WHERE id = ?
|
||||
''', (current_price, actual_return, result_desc, record_id))
|
||||
|
||||
conn.commit()
|
||||
logger.info(f"验证完成 {market}:{symbol}: {result_desc}")
|
||||
|
||||
except Exception as inner_e:
|
||||
logger.error(f"处理记录 {record_id} 失败: {inner_e}")
|
||||
# 标记为失败,避免重复处理
|
||||
# cursor.execute("UPDATE analysis_records SET status = 'FAILED' WHERE id = ?", (record_id,))
|
||||
# conn.commit()
|
||||
|
||||
conn.close()
|
||||
logger.info("反思验证周期结束")
|
||||
# 4. Write to memory system (agent learning)
|
||||
memory_situation = f"{market}:{symbol} auto-verified (analysis_date: {analysis_date})"
|
||||
memory_recommendation = f"Decision: {decision} (confidence {confidence}), reasoning: {(reasoning or '')[:120]}"
|
||||
memory_result = f"Verification: {result_desc}; return={actual_return:.2f}% (initial {initial_price} -> final {current_price})"
|
||||
|
||||
trader_memory.add_memory(
|
||||
memory_situation,
|
||||
memory_recommendation,
|
||||
memory_result,
|
||||
actual_return,
|
||||
metadata={
|
||||
"market": market,
|
||||
"symbol": symbol,
|
||||
"timeframe": "1D",
|
||||
"features": {
|
||||
"source": "auto_verify",
|
||||
"decision": decision,
|
||||
"confidence": confidence,
|
||||
"initial_price": initial_price,
|
||||
"final_price": current_price,
|
||||
"analysis_date": str(analysis_date),
|
||||
"result_desc": result_desc,
|
||||
"is_good_prediction": bool(is_good_prediction),
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
# 5. Update record status
|
||||
cur.execute(
|
||||
"""
|
||||
UPDATE qd_reflection_records
|
||||
SET status = 'COMPLETED', final_price = ?, actual_return = ?, check_result = ?
|
||||
WHERE id = ?
|
||||
""",
|
||||
(current_price, actual_return, result_desc, record_id)
|
||||
)
|
||||
conn.commit()
|
||||
logger.info(f"Verification completed {market}:{symbol}: {result_desc}")
|
||||
|
||||
except Exception as inner_e:
|
||||
logger.error(f"Failed to process record {record_id}: {inner_e}")
|
||||
# Optionally mark as failed to avoid repeated processing
|
||||
# cur.execute("UPDATE qd_reflection_records SET status = 'FAILED' WHERE id = ?", (record_id,))
|
||||
# conn.commit()
|
||||
|
||||
cur.close()
|
||||
logger.info("Reflection verification cycle completed")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"执行验证周期失败: {e}")
|
||||
logger.error(f"Failed to execute verification cycle: {e}")
|
||||
|
||||
def get_pending_count(self) -> int:
|
||||
"""Get count of pending verification records."""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cur = conn.cursor()
|
||||
cur.execute("SELECT COUNT(*) as cnt FROM qd_reflection_records WHERE status = 'PENDING'")
|
||||
count = cur.fetchone()['cnt']
|
||||
cur.close()
|
||||
return count
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get pending count: {e}")
|
||||
return 0
|
||||
|
||||
def get_statistics(self) -> Dict[str, Any]:
|
||||
"""Get reflection statistics."""
|
||||
try:
|
||||
with get_db_connection() as conn:
|
||||
cur = conn.cursor()
|
||||
|
||||
cur.execute("SELECT COUNT(*) as cnt FROM qd_reflection_records")
|
||||
total = cur.fetchone()['cnt']
|
||||
|
||||
cur.execute("SELECT COUNT(*) as cnt FROM qd_reflection_records WHERE status = 'PENDING'")
|
||||
pending = cur.fetchone()['cnt']
|
||||
|
||||
cur.execute("SELECT COUNT(*) as cnt FROM qd_reflection_records WHERE status = 'COMPLETED'")
|
||||
completed = cur.fetchone()['cnt']
|
||||
|
||||
cur.execute(
|
||||
"SELECT AVG(actual_return) as avg_ret FROM qd_reflection_records WHERE status = 'COMPLETED' AND actual_return IS NOT NULL"
|
||||
)
|
||||
avg_return = cur.fetchone()['avg_ret'] or 0
|
||||
|
||||
cur.close()
|
||||
|
||||
return {
|
||||
'total_records': total,
|
||||
'pending_records': pending,
|
||||
'completed_records': completed,
|
||||
'average_return': round(avg_return, 2)
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get statistics: {e}")
|
||||
return {}
|
||||
|
||||
Reference in New Issue
Block a user