aipa-cli 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,175 @@
1
+ """Persona definitions and registry for multi-agent support."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+
7
+ from aipriceaction.system import get_system_prompt
8
+
9
+
10
+ @dataclass
11
+ class Persona:
12
+ """An agent persona with custom instructions."""
13
+
14
+ id: str
15
+ name: str
16
+ description: str
17
+ extra_instructions: str = ""
18
+ include_data_policy: bool = True
19
+ include_analysis_framework: bool = True
20
+ include_ma_score: bool = True
21
+ include_disclaimer: bool = True
22
+
23
+ def build_system_prompt(self, lang: str) -> str:
24
+ """Build the full system prompt for this persona."""
25
+ prompt = get_system_prompt(
26
+ lang,
27
+ include_data_policy=self.include_data_policy,
28
+ include_analysis_framework=self.include_analysis_framework,
29
+ include_ma_score=self.include_ma_score,
30
+ include_disclaimer=self.include_disclaimer,
31
+ include_persona=True,
32
+ )
33
+ if self.extra_instructions:
34
+ prompt += "\n\n" + self.extra_instructions
35
+ return prompt
36
+
37
+
38
+ # -- Bilingual extra instructions --
39
+
40
+ _GENERAL_INSTRUCTIONS = {
41
+ "en": """## Tool Usage
42
+
43
+ You have tools to fetch OHLCV data and list available tickers:
44
+ - `get_ohlcv_data`: Fetch price data for any ticker with MA indicators and scores.
45
+ - `get_ticker_list`: Discover available tickers grouped by sector/industry.
46
+
47
+ When the user asks about a specific ticker, market sector, or price-related question, you MUST call the relevant tools before answering. Do not answer from memory alone — always fetch fresh data.
48
+
49
+ For non-market questions (greetings, general knowledge, etc.), respond naturally without calling tools.
50
+
51
+ ## Research Workflow (when analyzing tickers)
52
+ 1. Call `get_ohlcv_data` for each ticker mentioned by the user.
53
+ 2. If relevant, call `get_ticker_list` to discover related tickers in the same sector.
54
+ 3. Base your analysis ONLY on the tool results.
55
+ 4. Assess: trend direction, VPA signals, MA score momentum, volume confirmation.
56
+ 5. Structure your answer clearly with specific data points.
57
+ 6. Include the investment disclaimer at the end of any financial analysis.""",
58
+ "vn": """## Sử Dụng Công Cụ
59
+
60
+ Bạn có các công cụ để lấy dữ liệu OHLCV và danh sách mã chứng khoán:
61
+ - `get_ohlcv_data`: Lấy dữ liệu giá cho bất kỳ mã nào với chỉ báo MA và điểm số.
62
+ - `get_ticker_list`: Khám phá các mã chứng khoán theo nhóm ngành.
63
+
64
+ Khi người dùng hỏi về mã cụ thể, ngành, hoặc câu hỏi liên quan giá — bạn PHẢI gọi tools trước khi trả lời. Không trả lời từ trí nhớ — luôn lấy dữ liệu mới nhất.
65
+
66
+ Với câu hỏi ngoài thị trường (chào hỏi, kiến thức chung), trả lời tự nhiên không cần gọi tools.
67
+
68
+ ## Quy Trình Nghiên Cứu (khi phân tích mã)
69
+ 1. Gọi `get_ohlcv_data` cho mỗi mã người dùng nhắc đến.
70
+ 2. Nếu cần, gọi `get_ticker_list` để tìm mã cùng ngành.
71
+ 3. Phân tích CHỈ dựa trên kết quả tools.
72
+ 4. Đánh giá: xu hướng, tín hiệu VPA, động lực MA score, khối lượng.
73
+ 5. Cấu trúc câu trả lời rõ ràng với số liệu cụ thể.
74
+ 6. Bao gồm tuyên bố miễn trách nhiệm đầu tư cuối phân tích.""",
75
+ }
76
+
77
+ _ANALYST_INSTRUCTIONS = {
78
+ "en": """## Tool Usage
79
+
80
+ You have tools to fetch OHLCV data and list available tickers:
81
+ - `get_ohlcv_data`: Fetch price data for any ticker with MA indicators and scores.
82
+ - `get_ticker_list`: Discover available tickers grouped by sector/industry.
83
+
84
+ ## Research Workflow (MANDATORY)
85
+ 1. First, call `get_ohlcv_data` for EACH ticker explicitly mentioned in the user question.
86
+ 2. Then, call `get_ticker_list` to discover other tickers in the same sectors/industries.
87
+ 3. Call `get_ohlcv_data` for at least 2-3 additional tickers per sector to enable meaningful comparison. Do NOT skip this step.
88
+ 4. For each ticker, assess: trend direction, VPA signals (accumulation/distribution), MA score momentum across timeframes, volume confirmation, and support/resistance.
89
+ 5. Structure your final answer with:
90
+ - Per-ticker analysis with specific data points from the tool results
91
+ - Sector rotation observations (which sectors are leading/lagging)
92
+ - Multi-ticker ranking table
93
+ 6. Include the investment disclaimer at the end.
94
+
95
+ FAILURE TO CALL TOOLS = INVALID RESPONSE.""",
96
+ "vn": """## Sử Dụng Công Cụ
97
+
98
+ Bạn có các công cụ để lấy dữ liệu OHLCV và danh sách mã:
99
+ - `get_ohlcv_data`: Lấy dữ liệu giá cho bất kỳ mã nào với chỉ báo MA.
100
+ - `get_ticker_list`: Khám phá các mã theo nhóm ngành.
101
+
102
+ ## Quy Trình Nghiên Cứu (BẮT BUỘC)
103
+ 1. Gọi `get_ohlcv_data` cho MỖI mã được nhắc đến trong câu hỏi.
104
+ 2. Gọi `get_ticker_list` để tìm mã cùng ngành.
105
+ 3. Gọi `get_ohlcv_data` cho ít nhất 2-3 mã thêm mỗi ngành để so sánh. KHÔNG được bỏ qua bước này.
106
+ 4. Mỗi mã: đánh giá xu hướng, tín hiệu VPA, động lực MA score, khối lượng, hỗ trợ/kháng cự.
107
+ 5. Cấu trúc câu trả lời:
108
+ - Phân tích từng mã với số liệu cụ thể từ tools
109
+ - Quan sát luân chuyển ngành (ngành dẫn đầu/lagging)
110
+ - Bảng xếp hạng đa mã
111
+ 6. Bao gồm tuyên bố miễn trách nhiệm đầu tư ở cuối.
112
+
113
+ KHÔNG GỌI TOOL = PHẢN HỒI KHÔNG HỢP LỆ.""",
114
+ }
115
+
116
+
117
+ def _bilingual(texts: dict[str, str], lang: str) -> str:
118
+ return texts.get(lang, texts["en"])
119
+
120
+
121
+ class PersonaRegistry:
122
+ """Registry for agent personas."""
123
+
124
+ def __init__(self) -> None:
125
+ self._personas: dict[str, Persona] = {}
126
+ self._default_id: str = ""
127
+
128
+ def register(self, persona: Persona, *, is_default: bool = False) -> None:
129
+ self._personas[persona.id] = persona
130
+ if is_default or not self._default_id:
131
+ self._default_id = persona.id
132
+
133
+ def get(self, persona_id: str) -> Persona | None:
134
+ return self._personas.get(persona_id)
135
+
136
+ def list_personas(self) -> list[Persona]:
137
+ return list(self._personas.values())
138
+
139
+ @property
140
+ def default_id(self) -> str:
141
+ return self._default_id
142
+
143
+
144
+ def get_default_personas(lang: str = "en") -> PersonaRegistry:
145
+ """Return a PersonaRegistry with the built-in personas."""
146
+ registry = PersonaRegistry()
147
+
148
+ registry.register(
149
+ Persona(
150
+ id="general",
151
+ name="General Advisor",
152
+ description="Handles both market and non-market questions. Auto-calls tools when needed.",
153
+ extra_instructions=_bilingual(_GENERAL_INSTRUCTIONS, lang),
154
+ ),
155
+ is_default=True,
156
+ )
157
+
158
+ registry.register(
159
+ Persona(
160
+ id="analyst",
161
+ name="Deep Analyst",
162
+ description="Deep multi-ticker specialist with mandatory research workflow.",
163
+ extra_instructions=_bilingual(_ANALYST_INSTRUCTIONS, lang),
164
+ ),
165
+ )
166
+
167
+ return registry
168
+
169
+
170
+ def get_default_persona(lang: str = "en") -> Persona:
171
+ """Return the default (general) persona."""
172
+ registry = get_default_personas(lang)
173
+ persona = registry.get(registry.default_id)
174
+ assert persona is not None
175
+ return persona
@@ -0,0 +1,152 @@
1
+ """Tool registry and built-in tools for the agent."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass, field
6
+ from typing import TYPE_CHECKING
7
+
8
+ from langchain_core.tools import tool
9
+
10
+ if TYPE_CHECKING:
11
+ from aipriceaction import AIPriceAction, AIContextBuilder
12
+
13
+
14
+ @dataclass
15
+ class ToolDef:
16
+ """Wraps a LangChain BaseTool with metadata."""
17
+
18
+ tool: object # BaseTool
19
+ name: str
20
+ description: str
21
+ category: str = "general"
22
+
23
+
24
+ class ToolRegistry:
25
+ """Registry for agent tools."""
26
+
27
+ def __init__(self) -> None:
28
+ self._tools: list[ToolDef] = []
29
+
30
+ def register(self, tool_def: ToolDef) -> None:
31
+ self._tools.append(tool_def)
32
+
33
+ def unregister(self, name: str) -> None:
34
+ self._tools = [t for t in self._tools if t.name != name]
35
+
36
+ def get_tools(self, category: str | None = None) -> list[object]:
37
+ if category is None:
38
+ return [t.tool for t in self._tools]
39
+ return [t.tool for t in self._tools if t.category == category]
40
+
41
+ def get_tool_names(self, category: str | None = None) -> list[str]:
42
+ if category is None:
43
+ return [t.name for t in self._tools]
44
+ return [t.name for t in self._tools if t.category == category]
45
+
46
+
47
+ # -- Lazy singletons (mirrors langchain_agent.py pattern) --
48
+
49
+ _client: AIPriceAction | None = None
50
+ _builder: AIContextBuilder | None = None
51
+
52
+
53
+ def _ensure_clients(lang: str = "en") -> tuple[AIPriceAction, AIContextBuilder]:
54
+ global _client, _builder
55
+ if _client is None:
56
+ from aipriceaction import AIPriceAction
57
+ _client = AIPriceAction()
58
+ if _builder is None or _builder._lang != lang:
59
+ from aipriceaction import AIContextBuilder
60
+ _builder = AIContextBuilder(lang=lang)
61
+ return _client, _builder
62
+
63
+
64
+ def _reset_clients() -> None:
65
+ global _client, _builder
66
+ _client = None
67
+ _builder = None
68
+
69
+
70
+ # -- Tool factories --
71
+
72
+
73
+ def create_ohlcv_tool(lang: str = "en") -> ToolDef:
74
+ """Factory: creates the get_ohlcv_data tool."""
75
+
76
+ @tool
77
+ def get_ohlcv_data(ticker: str, interval: str = "1D", limit: int = 5) -> str:
78
+ """Fetch OHLCV data for a ticker. Returns formatted context with MA indicators and scores.
79
+
80
+ Args:
81
+ ticker: Ticker symbol (e.g. VCB, FPT, BTCUSDT).
82
+ interval: Time interval — "1D" (default), "1h", or "1m".
83
+ limit: Number of bars to return (default 5).
84
+ """
85
+ _, builder = _ensure_clients(lang)
86
+ try:
87
+ ctx = builder.build(
88
+ ticker=ticker,
89
+ interval=interval,
90
+ limit=limit,
91
+ reference_ticker=None,
92
+ include_system_prompt=False,
93
+ )
94
+ except Exception as e:
95
+ return f"Error fetching {ticker}: {e}"
96
+ if not ctx.strip():
97
+ return f"No data found for {ticker} ({interval})."
98
+ return ctx
99
+
100
+ return ToolDef(
101
+ tool=get_ohlcv_data,
102
+ name="get_ohlcv_data",
103
+ description="Fetch OHLCV data for a ticker with MA indicators and scores.",
104
+ category="market_data",
105
+ )
106
+
107
+
108
+ def create_ticker_list_tool(lang: str = "en") -> ToolDef:
109
+ """Factory: creates the get_ticker_list tool."""
110
+
111
+ @tool
112
+ def get_ticker_list(source: str | None = None) -> str:
113
+ """List available ticker symbols and metadata.
114
+
115
+ Args:
116
+ source: Filter by source — "vn", "yahoo", "crypto", "sjc". None = all.
117
+ """
118
+ client, _ = _ensure_clients(lang)
119
+ tickers = client.get_tickers(source=source)
120
+ if not tickers:
121
+ return "No tickers found."
122
+
123
+ from collections import Counter
124
+
125
+ source_counts = Counter(t.source for t in tickers)
126
+ group_counts = Counter(t.group for t in tickers if t.group)
127
+
128
+ lines = [f"Available tickers (source={source or 'all'}), total: {len(tickers)}"]
129
+ lines.append("Counts by source: " + ", ".join(f"{s}={c}" for s, c in source_counts.most_common()))
130
+ lines.append("Groups: " + ", ".join(f"{g}={c}" for g, c in group_counts.most_common(15)))
131
+ lines.append("")
132
+
133
+ # Symbols only, comma-separated for compactness
134
+ symbols = [t.ticker for t in tickers]
135
+ lines.append("Symbols: " + ", ".join(symbols))
136
+
137
+ return "\n".join(lines)
138
+
139
+ return ToolDef(
140
+ tool=get_ticker_list,
141
+ name="get_ticker_list",
142
+ description="List available ticker symbols and metadata.",
143
+ category="market_data",
144
+ )
145
+
146
+
147
+ def get_default_tools(lang: str = "en") -> ToolRegistry:
148
+ """Return a ToolRegistry pre-loaded with the built-in market data tools."""
149
+ registry = ToolRegistry()
150
+ registry.register(create_ohlcv_tool(lang))
151
+ registry.register(create_ticker_list_tool(lang))
152
+ return registry
@@ -0,0 +1,97 @@
1
+ """Main application: TabbedContent shell with shared state."""
2
+
3
+ import asyncio
4
+
5
+ from textual import work
6
+ from textual.app import App, ComposeResult
7
+ from textual.reactive import reactive
8
+ from textual.widgets import TabbedContent, TabPane, Header, Footer, Input, Select
9
+
10
+ from .bindings import BINDINGS
11
+ from .actions import AppActions
12
+ from .theme import AI_GREEN, SCREEN_CSS
13
+ from .chat import ChatTab
14
+ from .workflows import WorkflowsTab
15
+ from .ticker_data import TickerDataTab
16
+ from .settings_tab import SettingsTab
17
+ from .user_settings import load_settings
18
+
19
+
20
+ class AIPriceActionApp(AppActions, App):
21
+ """AIPriceAction Terminal TUI."""
22
+
23
+ TITLE = "AIPriceAction Terminal"
24
+ SUB_TITLE = "AI-powered ticker analysis"
25
+ CSS = SCREEN_CSS
26
+ BINDINGS = BINDINGS
27
+
28
+ # Reactive state shared across tabs
29
+ ticker: reactive[str] = reactive("VNINDEX")
30
+ interval: reactive[str] = reactive("1D")
31
+ language: reactive[str] = reactive("en")
32
+ ticker_options: reactive[list[tuple[str, str]]] = reactive(list)
33
+
34
+ def on_mount(self) -> None:
35
+ self.register_theme(AI_GREEN)
36
+ self.theme = "ai-green"
37
+ saved = load_settings()
38
+ self.ticker = saved["ticker"]
39
+ self.interval = saved["interval"]
40
+ self.language = saved["language"]
41
+ from aipriceaction import AIContextBuilder
42
+ from aipriceaction import AIPriceAction as AAPClient
43
+ self.builder = AIContextBuilder(lang=self.language)
44
+ self.client = AAPClient()
45
+ from .agents import AgentSession, AgentConfig
46
+ self.agent = AgentSession(AgentConfig(lang=self.language))
47
+ self._load_ticker_options()
48
+ # Populate SettingsTab widgets with loaded values
49
+ self.query_one("#setting-ticker", Input).value = self.ticker
50
+ self.query_one("#setting-interval", Select).value = self.interval
51
+ self.query_one("#setting-language", Select).value = self.language
52
+ self.query_one("#chat-input-field", Input).focus()
53
+
54
+ @work(exclusive=True)
55
+ async def _load_ticker_options(self) -> None:
56
+ """Load ticker list from SDK and populate ticker_options reactive."""
57
+ try:
58
+ tickers = await asyncio.to_thread(self.client.get_tickers)
59
+ source_tags = {"vn": "[VN]", "crypto": "[CRYPTO]", "yahoo": "[YH]", "sjc": "[SJC]"}
60
+ options = []
61
+ for t in tickers:
62
+ tag = source_tags.get(t.source, f"[{t.source.upper()}]")
63
+ label = f"{tag} {t.ticker}"
64
+ if t.name:
65
+ label += f" - {t.name}"
66
+ options.append((label, t.ticker))
67
+ options.sort(key=lambda x: x[0])
68
+ self.ticker_options = options
69
+ except Exception as e:
70
+ self.notify(f"Failed to load tickers: {e}", severity="error")
71
+
72
+ def compose(self) -> ComposeResult:
73
+ yield Header(show_clock=True)
74
+ with TabbedContent(initial="chat"):
75
+ with TabPane("Chat", id="chat"):
76
+ yield ChatTab()
77
+ with TabPane("Vietnam", id="tickers-vn"):
78
+ yield TickerDataTab(mode="vn")
79
+ with TabPane("Crypto", id="tickers-crypto"):
80
+ yield TickerDataTab(mode="crypto")
81
+ with TabPane("Global", id="tickers-global"):
82
+ yield TickerDataTab(mode="global")
83
+ with TabPane("Workflows", id="workflows"):
84
+ yield WorkflowsTab()
85
+ with TabPane("Settings", id="settings"):
86
+ yield SettingsTab()
87
+ yield Footer()
88
+
89
+
90
+ def main():
91
+ """Entry point."""
92
+ app = AIPriceActionApp()
93
+ app.run()
94
+
95
+
96
+ if __name__ == "__main__":
97
+ main()
@@ -0,0 +1,25 @@
1
+ """Key bindings and tab shortcuts for AIPriceActionApp."""
2
+
3
+ from textual.binding import Binding, BindingType
4
+
5
+ _TAB_KEYS: dict[str, str] = {
6
+ "1": "chat",
7
+ "2": "tickers-vn",
8
+ "3": "tickers-crypto",
9
+ "4": "tickers-global",
10
+ "5": "workflows",
11
+ "6": "settings",
12
+ }
13
+
14
+ BINDINGS: list[BindingType] = [
15
+ Binding("ctrl+q", "confirm_quit", "Quit", key_display="ctrl+q", priority=True),
16
+ Binding("1", "switch_tab('chat')", "Chat", key_display="1"),
17
+ Binding("2", "switch_tab('tickers-vn')", "Vietnam", key_display="2"),
18
+ Binding("3", "switch_tab('tickers-crypto')", "Crypto", key_display="3"),
19
+ Binding("4", "switch_tab('tickers-global')", "Global", key_display="4"),
20
+ Binding("5", "switch_tab('workflows')", "Workflows", key_display="5"),
21
+ Binding("6", "switch_tab('settings')", "Settings", key_display="6"),
22
+ Binding("escape", "focus_none", "Back", key_display="esc", priority=True),
23
+ Binding("enter", "focus_first_input", "Focus input", key_display="enter"),
24
+ Binding("?", "show_help", "Help", key_display="?"),
25
+ ]