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.
- aipa_cli-0.1.0.dist-info/METADATA +69 -0
- aipa_cli-0.1.0.dist-info/RECORD +28 -0
- aipa_cli-0.1.0.dist-info/WHEEL +4 -0
- aipa_cli-0.1.0.dist-info/entry_points.txt +2 -0
- aipa_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- aipriceaction_terminal/__init__.py +3 -0
- aipriceaction_terminal/__main__.py +3 -0
- aipriceaction_terminal/actions.py +73 -0
- aipriceaction_terminal/agents/__init__.py +20 -0
- aipriceaction_terminal/agents/agent.py +175 -0
- aipriceaction_terminal/agents/callbacks.py +202 -0
- aipriceaction_terminal/agents/config.py +35 -0
- aipriceaction_terminal/agents/personas.py +175 -0
- aipriceaction_terminal/agents/tools.py +152 -0
- aipriceaction_terminal/app.py +97 -0
- aipriceaction_terminal/bindings.py +25 -0
- aipriceaction_terminal/chat.py +345 -0
- aipriceaction_terminal/cli.py +54 -0
- aipriceaction_terminal/cli_commands.py +51 -0
- aipriceaction_terminal/settings_tab.py +76 -0
- aipriceaction_terminal/theme.py +29 -0
- aipriceaction_terminal/ticker_data.py +33 -0
- aipriceaction_terminal/user_settings.py +27 -0
- aipriceaction_terminal/utils.py +29 -0
- aipriceaction_terminal/widgets/__init__.py +6 -0
- aipriceaction_terminal/widgets/chat_input.py +179 -0
- aipriceaction_terminal/widgets/ticker_select.py +168 -0
- aipriceaction_terminal/workflows.py +172 -0
|
@@ -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
|
+
]
|