erlangshen 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/.claude/agents/equity-agent.md +26 -0
  2. package/.claude/agents/macro-agent.md +25 -0
  3. package/.claude/commands/analyze.md +40 -0
  4. package/.claude/commands/macro.md +29 -0
  5. package/.claude/settings.json +12 -0
  6. package/CODEX_GOAL.md +46 -0
  7. package/README.md +206 -0
  8. package/bin/cli.js +67 -0
  9. package/bin/erlangshen +2 -0
  10. package/bin/xiaoergod +2 -0
  11. package/frontend/index.html +700 -0
  12. package/knowledge/crypto_guide.md +147 -0
  13. package/knowledge/economic_indicators.md +125 -0
  14. package/knowledge/financial_glossary.md +148 -0
  15. package/knowledge/first_principles.md +50 -0
  16. package/knowledge/first_principles_deep.md +115 -0
  17. package/knowledge/global_markets.md +173 -0
  18. package/knowledge/insights.md +141 -0
  19. package/knowledge/market_basics.md +116 -0
  20. package/knowledge/memos/session_20260513_003616.json +6 -0
  21. package/knowledge/memos/session_20260513_003822.json +6 -0
  22. package/knowledge/risk_management.md +151 -0
  23. package/knowledge/team_context.md +42 -0
  24. package/knowledge/trading_strategies.md +114 -0
  25. package/package.json +42 -0
  26. package/requirements.txt +14 -0
  27. package/scripts/postinstall.js +188 -0
  28. package/scripts/preuninstall.js +22 -0
  29. package/src/__init__.py +4 -0
  30. package/src/__pycache__/__init__.cpython-313.pyc +0 -0
  31. package/src/agents/__init__.py +3 -0
  32. package/src/agents/base.py +103 -0
  33. package/src/agents/base_agent.py +86 -0
  34. package/src/agents/equity.py +136 -0
  35. package/src/agents/equity_agent.py +91 -0
  36. package/src/agents/erlang.py +165 -0
  37. package/src/agents/macro.py +137 -0
  38. package/src/agents/macro_agent.py +81 -0
  39. package/src/agents/multi_asset.py +147 -0
  40. package/src/agents/multi_asset_agent.py +87 -0
  41. package/src/api/__init__.py +1 -0
  42. package/src/api/__pycache__/__init__.cpython-313.pyc +0 -0
  43. package/src/api/__pycache__/server.cpython-313.pyc +0 -0
  44. package/src/api/cli.py +435 -0
  45. package/src/api/cli_enhanced.py +537 -0
  46. package/src/api/server.py +266 -0
  47. package/src/brain.py +200 -0
  48. package/src/cli.py +153 -0
  49. package/src/commands/__init__.py +3 -0
  50. package/src/commands/analyze.py +131 -0
  51. package/src/commands/macro.py +100 -0
  52. package/src/commands/memo.py +216 -0
  53. package/src/commands/portfolio.py +154 -0
  54. package/src/commands/report.py +228 -0
  55. package/src/commands/risk.py +183 -0
  56. package/src/commands/search.py +183 -0
  57. package/src/commands/stock.py +124 -0
  58. package/src/config.py +327 -0
  59. package/src/core/__init__.py +1 -0
  60. package/src/core/brain.py +645 -0
  61. package/src/core/cerebellum.py +175 -0
  62. package/src/core/investment_universe.py +423 -0
  63. package/src/core/knowledge.py +207 -0
  64. package/src/core/memory.py +115 -0
  65. package/src/hooks/__init__.py +3 -0
  66. package/src/hooks/session_end.py +57 -0
  67. package/src/hooks/session_start.py +75 -0
  68. package/src/knowledge/__init__.py +1 -0
  69. package/src/mcp/__init__.py +3 -0
  70. package/src/mcp/feishu.py +331 -0
  71. package/src/mcp/fund_tools.py +323 -0
  72. package/src/mcp/macro.py +452 -0
  73. package/src/mcp/market.py +331 -0
  74. package/src/mcp/registry.py +168 -0
  75. package/src/network/__init__.py +15 -0
  76. package/src/network/detector.py +125 -0
  77. package/src/network/proxy.py +199 -0
  78. package/src/network/router.py +103 -0
  79. package/src/prompts/__init__.py +1 -0
  80. package/src/prompts/analysis_framework.md +164 -0
  81. package/src/prompts/persona.md +65 -0
  82. package/src/prompts/report_template.md +144 -0
  83. package/src/skills/__init__.py +3 -0
  84. package/src/skills/framework.py +105 -0
  85. package/src/skills/templates.py +342 -0
  86. package/src/tools/__init__.py +1 -0
  87. package/src/tools/file_tools.py +209 -0
  88. package/src/tools/macro_tools.py +152 -0
  89. package/src/tools/market_tools.py +1172 -0
  90. package/src/tools/registry.py +398 -0
  91. package/src/tools/search_tools.py +777 -0
  92. package/tests/__init__.py +1 -0
  93. package/tests/test_erlangshen.py +140 -0
@@ -0,0 +1,207 @@
1
+ """
2
+ Knowledge - 知识库管理
3
+ 基于向量的语义搜索知识库
4
+ """
5
+ import os
6
+ import json
7
+ import time
8
+ from datetime import datetime
9
+ from pathlib import Path
10
+ from typing import Optional
11
+ from pydantic import BaseModel, Field
12
+ from loguru import logger
13
+
14
+
15
+ class KnowledgeEntry(BaseModel):
16
+ """知识条目"""
17
+ entry_id: str
18
+ content: str
19
+ category: str = "general"
20
+ tags: list[str] = Field(default_factory=list)
21
+ created_at: float = Field(default_factory=time.time)
22
+ updated_at: float = Field(default_factory=time.time)
23
+ metadata: dict = Field(default_factory=dict)
24
+
25
+
26
+ class KnowledgeBase:
27
+ """
28
+ 知识库管理
29
+
30
+ 提供:
31
+ - 添加知识条目
32
+ - 语义搜索
33
+ - 分类管理
34
+ - 知识沉淀
35
+ """
36
+
37
+ def __init__(self, base_path: Optional[str] = None):
38
+ if base_path is None:
39
+ base_path = Path(__file__).parent.parent.parent / "knowledge"
40
+ self.base_path = Path(base_path)
41
+ self.memos_path = self.base_path / "memos"
42
+ self.reports_path = self.base_path / "reports"
43
+ self.insights_path = self.base_path / "insights"
44
+ self.facts_path = self.base_path / "facts"
45
+
46
+ # 确保目录存在
47
+ for p in [self.memos_path, self.reports_path, self.insights_path, self.facts_path]:
48
+ p.mkdir(parents=True, exist_ok=True)
49
+
50
+ # 内存索引
51
+ self._index: list[KnowledgeEntry] = []
52
+ self._load_index()
53
+ logger.info(f"KnowledgeBase initialized at {self.base_path}")
54
+
55
+ def _load_index(self) -> None:
56
+ """加载知识库索引"""
57
+ index_file = self.base_path / "index.json"
58
+ if index_file.exists():
59
+ try:
60
+ data = json.loads(index_file.read_text())
61
+ self._index = [KnowledgeEntry(**e) for e in data]
62
+ logger.info(f"Loaded {len(self._index)} entries from index")
63
+ except Exception as e:
64
+ logger.warning(f"Failed to load index: {e}")
65
+
66
+ def _save_index(self) -> None:
67
+ """保存知识库索引"""
68
+ index_file = self.base_path / "index.json"
69
+ try:
70
+ data = [e.model_dump() for e in self._index]
71
+ index_file.write_text(json.dumps(data, ensure_ascii=False, indent=2))
72
+ except Exception as e:
73
+ logger.error(f"Failed to save index: {e}")
74
+
75
+ def add(
76
+ self,
77
+ content: str,
78
+ category: str = "general",
79
+ tags: Optional[list[str]] = None,
80
+ metadata: Optional[dict] = None,
81
+ ) -> KnowledgeEntry:
82
+ """
83
+ 添加知识条目
84
+
85
+ Args:
86
+ content: 知识内容
87
+ category: 分类 (memo/report/insight/fact)
88
+ tags: 标签
89
+ metadata: 额外元数据
90
+
91
+ Returns:
92
+ KnowledgeEntry 新增的条目
93
+ """
94
+ entry = KnowledgeEntry(
95
+ entry_id=f"kb_{int(time.time()*1000)}",
96
+ content=content,
97
+ category=category,
98
+ tags=tags or [],
99
+ metadata=metadata or {},
100
+ )
101
+ self._index.append(entry)
102
+ self._save_index()
103
+ logger.info(f"Added knowledge entry: {entry.entry_id} [{category}]")
104
+ return entry
105
+
106
+ async def search(
107
+ self,
108
+ query: str,
109
+ top_k: int = 5,
110
+ category: Optional[str] = None,
111
+ ) -> list[dict]:
112
+ """
113
+ 搜索知识库 (简单关键词匹配,实际可接入向量数据库)
114
+
115
+ Args:
116
+ query: 搜索查询
117
+ top_k: 返回数量
118
+ category: 限定分类
119
+
120
+ Returns:
121
+ list[dict] 匹配的知识条目
122
+ """
123
+ query_lower = query.lower()
124
+ results = []
125
+
126
+ for entry in reversed(self._index): # 优先返回最新的
127
+ if category and entry.category != category:
128
+ continue
129
+
130
+ # 简单评分:内容匹配度
131
+ score = 0.0
132
+ query_words = query_lower.split()
133
+ content_lower = entry.content.lower()
134
+
135
+ for word in query_words:
136
+ if word in content_lower:
137
+ score += 1.0
138
+ if word in entry.tags:
139
+ score += 2.0 # 标签匹配权重更高
140
+
141
+ if score > 0:
142
+ results.append({
143
+ "entry_id": entry.entry_id,
144
+ "content": entry.content,
145
+ "category": entry.category,
146
+ "tags": entry.tags,
147
+ "score": score,
148
+ "created_at": entry.created_at,
149
+ })
150
+
151
+ # 排序并返回top_k
152
+ results.sort(key=lambda x: x["score"], reverse=True)
153
+ return results[:top_k]
154
+
155
+ async def write_memo(self, content: str, title: Optional[str] = None) -> str:
156
+ """写入纪要"""
157
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
158
+ title_str = title or "memo"
159
+ filename = self.memos_path / f"{timestamp}_{title_str}.md"
160
+ filename.write_text(content, encoding="utf-8")
161
+
162
+ # 同时添加到索引
163
+ self.add(content, category="memo", metadata={"title": title or "untitled"})
164
+ logger.info(f"Wrote memo: {filename.name}")
165
+ return str(filename)
166
+
167
+ async def write_report(self, content: str, title: str) -> str:
168
+ """写入报告"""
169
+ timestamp = datetime.now().strftime("%Y%m%d")
170
+ filename = self.reports_path / f"{timestamp}_{title}.md"
171
+ filename.write_text(content, encoding="utf-8")
172
+
173
+ self.add(content, category="report", metadata={"title": title})
174
+ logger.info(f"Wrote report: {filename.name}")
175
+ return str(filename)
176
+
177
+ async def append_insight(self, insight: str, tags: Optional[list[str]] = None) -> KnowledgeEntry:
178
+ """追加洞察"""
179
+ return self.add(
180
+ content=insight,
181
+ category="insight",
182
+ tags=tags or ["insight"],
183
+ )
184
+
185
+ async def add_fact(self, fact: str, source: Optional[str] = None) -> KnowledgeEntry:
186
+ """添加事实"""
187
+ return self.add(
188
+ content=fact,
189
+ category="fact",
190
+ metadata={"source": source} if source else {},
191
+ )
192
+
193
+ def list_entries(self, category: Optional[str] = None) -> list[KnowledgeEntry]:
194
+ """列出知识条目"""
195
+ if category:
196
+ return [e for e in self._index if e.category == category]
197
+ return self._index
198
+
199
+ def stats(self) -> dict:
200
+ """知识库统计"""
201
+ categories = {}
202
+ for e in self._index:
203
+ categories[e.category] = categories.get(e.category, 0) + 1
204
+ return {
205
+ "total": len(self._index),
206
+ "by_category": categories,
207
+ }
@@ -0,0 +1,115 @@
1
+ """
2
+ Memory - 多层记忆系统
3
+ 工作记忆、情景记忆、语义记忆、程序记忆
4
+ """
5
+ import json
6
+ import time
7
+ from datetime import datetime
8
+ from typing import Any, Optional
9
+ from pydantic import BaseModel, Field
10
+ from loguru import logger
11
+
12
+
13
+ class Message(BaseModel):
14
+ """工作记忆中的单条消息"""
15
+ role: str # user, assistant, system
16
+ content: str
17
+ timestamp: float = Field(default_factory=time.time)
18
+
19
+
20
+ class Event(BaseModel):
21
+ """情景记忆中的事件"""
22
+ event_id: str
23
+ description: str
24
+ timestamp: float
25
+ tags: list[str] = Field(default_factory=list)
26
+ importance: float = 0.5 # 0-1
27
+
28
+
29
+ class Memory:
30
+ """
31
+ 多层记忆系统
32
+
33
+ - working_memory: 当前上下文中的消息
34
+ - episodic_memory: 重要事件记录
35
+ - semantic_memory: 语义知识(通过KnowledgeBase)
36
+ - procedural_memory: 能力技能注册
37
+ """
38
+
39
+ def __init__(self, max_working: int = 50):
40
+ self.max_working = max_working
41
+ self.working_memory: list[Message] = []
42
+ self.episodic_memory: list[Event] = []
43
+ self.procedural_memory: dict[str, Any] = {}
44
+ logger.info("Memory initialized")
45
+
46
+ async def add_message(self, role: str, content: str) -> None:
47
+ """添加工作记忆消息"""
48
+ msg = Message(role=role, content=content)
49
+ self.working_memory.append(msg)
50
+ if len(self.working_memory) > self.max_working:
51
+ self.working_memory = self.working_memory[-self.max_working:]
52
+ logger.debug(f"Added message to working memory: {role}")
53
+
54
+ async def add_interaction(self, query: str, response: str) -> None:
55
+ """记录一次交互"""
56
+ await self.add_message("user", query)
57
+ await self.add_message("assistant", response)
58
+
59
+ # 同时记录到情景记忆
60
+ event = Event(
61
+ event_id=f"evt_{int(time.time()*1000)}",
62
+ description=f"Query: {query[:100]} | Response: {response[:100]}",
63
+ timestamp=time.time(),
64
+ tags=["interaction"],
65
+ )
66
+ self.episodic_memory.append(event)
67
+ # 保留最近100个重要事件
68
+ if len(self.episodic_memory) > 100:
69
+ self.episodic_memory = self.episodic_memory[-100:]
70
+
71
+ async def get_context(self, last_n: Optional[int] = None) -> list[Message]:
72
+ """获取工作记忆上下文"""
73
+ if last_n:
74
+ return self.working_memory[-last_n:]
75
+ return self.working_memory
76
+
77
+ async def get_recent_events(self, hours: float = 24) -> list[Event]:
78
+ """获取最近的事件"""
79
+ now = time.time()
80
+ cutoff = now - hours * 3600
81
+ return [e for e in self.episodic_memory if e.timestamp >= cutoff]
82
+
83
+ def register_skill(self, name: str, skill: Any) -> None:
84
+ """注册程序记忆(技能)"""
85
+ self.procedural_memory[name] = skill
86
+
87
+ def get_skill(self, name: str) -> Optional[Any]:
88
+ """获取技能"""
89
+ return self.procedural_memory.get(name)
90
+
91
+ def list_skills(self) -> list[str]:
92
+ """列出所有已注册技能"""
93
+ return list(self.procedural_memory.keys())
94
+
95
+ async def search_episodic(self, keyword: str) -> list[Event]:
96
+ """搜索情景记忆"""
97
+ keyword = keyword.lower()
98
+ return [
99
+ e for e in self.episodic_memory
100
+ if keyword in e.description.lower()
101
+ ]
102
+
103
+ def export_state(self) -> dict:
104
+ """导出记忆状态(用于持久化)"""
105
+ return {
106
+ "episodic": [e.model_dump() for e in self.episodic_memory],
107
+ "procedural": list(self.procedural_memory.keys()),
108
+ "export_time": datetime.now().isoformat(),
109
+ }
110
+
111
+ def import_state(self, state: dict) -> None:
112
+ """导入记忆状态"""
113
+ if "episodic" in state:
114
+ self.episodic_memory = [Event(**e) for e in state["episodic"]]
115
+ logger.info(f"Imported {len(self.episodic_memory)} events into episodic memory")
@@ -0,0 +1,3 @@
1
+ """
2
+ Hooks 模块
3
+ """
@@ -0,0 +1,57 @@
1
+ """
2
+ SessionEnd 钩子 - 会话结束时执行
3
+ """
4
+
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+ from typing import Any
8
+ from src.brain import Brain
9
+ from src.mcp.registry import MCPRegistry
10
+
11
+
12
+ class SessionEndHook:
13
+ """
14
+ 会话结束钩子
15
+
16
+ 在会话结束时执行,用于:
17
+ - 保存会话摘要
18
+ - 更新知识库
19
+ - 清理临时文件
20
+ """
21
+
22
+ def __init__(self, brain: Brain, mcp: MCPRegistry):
23
+ self.brain = brain
24
+ self.mcp = mcp
25
+ self.session_summary_dir = Path("~/.openclaw-agent-06/workspace/erlangshen/knowledge/memos").expanduser()
26
+ self.session_summary_dir.mkdir(parents=True, exist_ok=True)
27
+
28
+ async def run(self) -> None:
29
+ """执行会话结束钩子"""
30
+ try:
31
+ # 生成会话摘要
32
+ summary = self._generate_summary()
33
+
34
+ # 保存会话摘要
35
+ await self._save_session_summary(summary)
36
+
37
+ except Exception as e:
38
+ print(f"会话结束处理完成 (部分操作失败: {e})")
39
+
40
+ def _generate_summary(self) -> dict:
41
+ """生成会话摘要"""
42
+ return {
43
+ "session_id": datetime.now().strftime("%Y%m%d_%H%M%S"),
44
+ "start_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
45
+ "conversations": [],
46
+ "commands_executed": [],
47
+ }
48
+
49
+ async def _save_session_summary(self, summary: dict) -> None:
50
+ """保存会话摘要"""
51
+ filename = f"session_{summary['session_id']}.json"
52
+ filepath = self.session_summary_dir / filename
53
+
54
+ import json
55
+
56
+ with open(filepath, "w", encoding="utf-8") as f:
57
+ json.dump(summary, f, ensure_ascii=False, indent=2)
@@ -0,0 +1,75 @@
1
+ """
2
+ SessionStart 钩子 - 会话开始时执行
3
+ """
4
+
5
+ from datetime import datetime
6
+ from typing import Any
7
+ from src.brain import Brain
8
+ from src.mcp.registry import MCPRegistry
9
+
10
+
11
+ class SessionStartHook:
12
+ """
13
+ 会话开始钩子
14
+
15
+ 在新会话开始时执行,用于:
16
+ - 加载市场快照
17
+ - 检查重要数据更新
18
+ - 初始化会话上下文
19
+ """
20
+
21
+ def __init__(self, brain: Brain, mcp: MCPRegistry):
22
+ self.brain = brain
23
+ self.mcp = mcp
24
+
25
+ async def run(self) -> None:
26
+ """执行会话开始钩子"""
27
+ try:
28
+ # 获取市场快照
29
+ snapshot = await self._get_market_snapshot()
30
+
31
+ # 显示欢迎信息
32
+ self._print_welcome(snapshot)
33
+
34
+ except Exception as e:
35
+ print(f"会话初始化完成 (部分数据加载失败: {e})")
36
+
37
+ async def _get_market_snapshot(self) -> dict:
38
+ """获取市场快照"""
39
+ snapshot = {
40
+ "indices": [],
41
+ "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
42
+ }
43
+
44
+ # 尝试获取主要指数
45
+ try:
46
+ index_codes = ["000001.SH", "399001.SZ", "399006.SZ", "000300.SH"]
47
+ for code in index_codes:
48
+ result = await self.mcp.call_tool("get_index_quote", index_code=code)
49
+ if isinstance(result, dict) and result.get("price"):
50
+ snapshot["indices"].append(result)
51
+ except Exception:
52
+ pass
53
+
54
+ return snapshot
55
+
56
+ def _print_welcome(self, snapshot: dict) -> None:
57
+ """打印欢迎信息"""
58
+ print(f"\n{'='*50}")
59
+ print(f" 二郎神投资分析智能体")
60
+ print(f"{'='*50}")
61
+ print(f" 时间: {snapshot['time']}")
62
+
63
+ if snapshot["indices"]:
64
+ print(f"\n 市场快照:")
65
+ for index in snapshot["indices"]:
66
+ name = index.get("name", "")
67
+ price = index.get("price", 0)
68
+ change = index.get("change_pct", 0)
69
+ arrow = "▲" if change >= 0 else "▼"
70
+ color = "" if not hasattr(self, '_has_color') else ("\033[92m" if change >= 0 else "\033[91m")
71
+ reset = "" if not hasattr(self, '_has_color') else "\033[0m"
72
+ print(f" {name}: {price:.2f} {arrow} {abs(change):.2f}%")
73
+
74
+ print(f"\n 输入 /help 查看可用命令")
75
+ print(f"{'='*50}\n")
@@ -0,0 +1 @@
1
+ """Knowledge base"""
@@ -0,0 +1,3 @@
1
+ """
2
+ MCP 模块
3
+ """