aipa-cli 0.1.5__tar.gz → 0.1.7__tar.gz

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 (41) hide show
  1. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/CHANGELOG.md +19 -0
  2. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/PKG-INFO +30 -4
  3. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/README.md +29 -3
  4. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/__init__.py +1 -1
  5. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/app.py +15 -2
  6. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/chat.py +15 -1
  7. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/cli.py +22 -1
  8. aipa_cli-0.1.7/src/aipriceaction_terminal/cli_setup.py +71 -0
  9. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/settings_tab.py +7 -2
  10. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/user_settings.py +20 -0
  11. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/.gitignore +0 -0
  12. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/LICENSE +0 -0
  13. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/pyproject.toml +0 -0
  14. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/__main__.py +0 -0
  15. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/actions.py +0 -0
  16. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/agents/__init__.py +0 -0
  17. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/agents/agent.py +0 -0
  18. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/agents/callbacks.py +0 -0
  19. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/agents/config.py +0 -0
  20. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/agents/personas.py +0 -0
  21. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/agents/tools.py +0 -0
  22. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/bindings.py +0 -0
  23. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/cli_commands.py +0 -0
  24. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/deep_research.py +0 -0
  25. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/theme.py +0 -0
  26. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/ticker_data.py +0 -0
  27. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/utils.py +0 -0
  28. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/widgets/__init__.py +0 -0
  29. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/widgets/chat_input.py +0 -0
  30. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/widgets/ticker_select.py +0 -0
  31. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/src/aipriceaction_terminal/workflows.py +0 -0
  32. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/tests/conftest.py +0 -0
  33. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/tests/openrouter_responses.py +0 -0
  34. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/tests/test_app.py +0 -0
  35. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/tests/test_chat.py +0 -0
  36. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/tests/test_integration.py +0 -0
  37. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/tests/test_settings_api.py +0 -0
  38. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/tests/test_thinking.py +0 -0
  39. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/tests/test_tool_call_streaming.py +0 -0
  40. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/tests/test_utils.py +0 -0
  41. {aipa_cli-0.1.5 → aipa_cli-0.1.7}/tests/test_workflows.py +0 -0
@@ -5,6 +5,25 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.1.7] - 2026-05-09
9
+
10
+ ### Fixed
11
+ - Bridge settings.json API key to SDK Pydantic settings by seeding env vars (`OPENAI_API_KEY`, `OPENAI_BASE_URL`, `OPENAI_MODEL`) at CLI entry point before any SDK import
12
+
13
+ ## [0.1.6] - 2026-05-09
14
+
15
+ ### Added
16
+ - Add `aipa setup` interactive CLI command for first-run configuration (language, reference ticker, API key, base URL, model)
17
+ - Add `setup_done` flag to user settings, auto-run setup before commands that need an API key (`analyze`, `deep-research`, TUI)
18
+ - Add "First-Run Setup" section to README documenting which commands require setup
19
+
20
+ ### Changed
21
+ - Lazy-load `AgentSession` in TUI instead of creating on mount, preventing crash when no API key is configured
22
+ - Guard agent usage in chat (`_run_agent_chat`, `_run_analyze`, `/clear`) and settings tab with `_ensure_agent()`
23
+
24
+ ### Fixed
25
+ - Fix crash on `aipa` launch when no API key is configured (`OpenAIError: Missing credentials`)
26
+
8
27
  ## [0.1.5] - 2026-05-09
9
28
 
10
29
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: aipa-cli
3
- Version: 0.1.5
3
+ Version: 0.1.7
4
4
  Summary: Terminal TUI for AI-powered ticker analysis
5
5
  Project-URL: Homepage, https://github.com/quanhua92/aipriceaction
6
6
  Project-URL: Repository, https://github.com/quanhua92/aipriceaction
@@ -46,15 +46,18 @@ aipa-cli
46
46
  ## Requirements
47
47
 
48
48
  - Python 3.13+
49
- - An OpenAI-compatible API key (`OPENAI_API_KEY`)
49
+ - An OpenAI-compatible API key (`OPENAI_API_KEY`) — only needed for AI analysis, not for data fetching
50
50
  - Optional: set `OPENAI_BASE_URL` for custom providers like OpenRouter
51
51
 
52
52
  ## Quick Start
53
53
 
54
54
  ```bash
55
- # Launch the TUI (chat, workflows, ticker browser)
55
+ # Launch the TUI first run auto-starts interactive setup
56
56
  aipa
57
57
 
58
+ # Or run setup manually at any time
59
+ aipa setup
60
+
58
61
  # AI analysis with default question template
59
62
  aipa analyze VCB
60
63
 
@@ -166,7 +169,7 @@ aipa deep-research --lang vn
166
169
 
167
170
  ### `aipa get-ohlcv-data`
168
171
 
169
- Fetch raw OHLCV data as a table (no LLM involved).
172
+ Fetch raw OHLCV data as a table (no LLM involved, works without setup).
170
173
 
171
174
  ```
172
175
  # Default: daily data with EMA indicators
@@ -191,6 +194,29 @@ aipa get-ohlcv-data BTCUSDT --interval 1D --limit 30
191
194
  | `--ma` / `--no-ma` | Include/exclude moving averages (default: included) |
192
195
  | `--ema` | Use EMA instead of SMA |
193
196
 
197
+ ### `aipa setup`
198
+
199
+ Interactive first-run configuration. Prompts for language, reference ticker, API key, base URL, and model. Settings are saved to `~/.aipriceaction/settings.json`. Re-running shows current values as defaults.
200
+
201
+ ```
202
+ # Run interactively
203
+ aipa setup
204
+ ```
205
+
206
+ ## First-Run Setup
207
+
208
+ Commands that require an API key will auto-run `aipa setup` on first use if not yet configured. Commands that don't need an API key always work immediately.
209
+
210
+ | Command | Setup required? |
211
+ |---|---|
212
+ | `aipa get-ohlcv-data` | No setup needed |
213
+ | `aipa analyze VCB --context-only` | No setup needed |
214
+ | `aipa analyze VCB --questions` | No setup needed |
215
+ | `aipa setup` | Runs setup |
216
+ | `aipa` | Auto-runs setup first |
217
+ | `aipa analyze VCB` | Auto-runs setup first |
218
+ | `aipa deep-research` | Auto-runs setup first |
219
+
194
220
  ## TUI
195
221
 
196
222
  Launch the TUI with `aipa`. The interface has three tabs:
@@ -21,15 +21,18 @@ aipa-cli
21
21
  ## Requirements
22
22
 
23
23
  - Python 3.13+
24
- - An OpenAI-compatible API key (`OPENAI_API_KEY`)
24
+ - An OpenAI-compatible API key (`OPENAI_API_KEY`) — only needed for AI analysis, not for data fetching
25
25
  - Optional: set `OPENAI_BASE_URL` for custom providers like OpenRouter
26
26
 
27
27
  ## Quick Start
28
28
 
29
29
  ```bash
30
- # Launch the TUI (chat, workflows, ticker browser)
30
+ # Launch the TUI first run auto-starts interactive setup
31
31
  aipa
32
32
 
33
+ # Or run setup manually at any time
34
+ aipa setup
35
+
33
36
  # AI analysis with default question template
34
37
  aipa analyze VCB
35
38
 
@@ -141,7 +144,7 @@ aipa deep-research --lang vn
141
144
 
142
145
  ### `aipa get-ohlcv-data`
143
146
 
144
- Fetch raw OHLCV data as a table (no LLM involved).
147
+ Fetch raw OHLCV data as a table (no LLM involved, works without setup).
145
148
 
146
149
  ```
147
150
  # Default: daily data with EMA indicators
@@ -166,6 +169,29 @@ aipa get-ohlcv-data BTCUSDT --interval 1D --limit 30
166
169
  | `--ma` / `--no-ma` | Include/exclude moving averages (default: included) |
167
170
  | `--ema` | Use EMA instead of SMA |
168
171
 
172
+ ### `aipa setup`
173
+
174
+ Interactive first-run configuration. Prompts for language, reference ticker, API key, base URL, and model. Settings are saved to `~/.aipriceaction/settings.json`. Re-running shows current values as defaults.
175
+
176
+ ```
177
+ # Run interactively
178
+ aipa setup
179
+ ```
180
+
181
+ ## First-Run Setup
182
+
183
+ Commands that require an API key will auto-run `aipa setup` on first use if not yet configured. Commands that don't need an API key always work immediately.
184
+
185
+ | Command | Setup required? |
186
+ |---|---|
187
+ | `aipa get-ohlcv-data` | No setup needed |
188
+ | `aipa analyze VCB --context-only` | No setup needed |
189
+ | `aipa analyze VCB --questions` | No setup needed |
190
+ | `aipa setup` | Runs setup |
191
+ | `aipa` | Auto-runs setup first |
192
+ | `aipa analyze VCB` | Auto-runs setup first |
193
+ | `aipa deep-research` | Auto-runs setup first |
194
+
169
195
  ## TUI
170
196
 
171
197
  Launch the TUI with `aipa`. The interface has three tabs:
@@ -1,4 +1,4 @@
1
1
  """AIPriceAction Terminal - TUI chat interface for ticker analysis."""
2
2
 
3
- __version__ = "0.1.5"
3
+ __version__ = "0.1.7"
4
4
 
@@ -38,12 +38,12 @@ class AIPriceActionApp(AppActions, App):
38
38
  self.ticker = saved["ticker"]
39
39
  self.interval = saved["interval"]
40
40
  self.language = saved["language"]
41
+ self.agent = None
42
+ self._agent_lang: str | None = None
41
43
  from aipriceaction import AIContextBuilder
42
44
  from aipriceaction import AIPriceAction as AAPClient
43
45
  self.builder = AIContextBuilder(lang=self.language)
44
46
  self.client = AAPClient()
45
- from .agents import AgentSession, AgentConfig
46
- self.agent = AgentSession(AgentConfig(lang=self.language))
47
47
  self._load_ticker_options()
48
48
  # Populate SettingsTab widgets with loaded values
49
49
  self.query_one("#setting-ticker", Input).value = self.ticker
@@ -69,6 +69,19 @@ class AIPriceActionApp(AppActions, App):
69
69
  except Exception as e:
70
70
  self.notify(f"Failed to load tickers: {e}", severity="error")
71
71
 
72
+ def _ensure_agent(self) -> bool:
73
+ """Lazy-create AgentSession on first use. Returns False if API key is missing."""
74
+ if self.agent is not None and self._agent_lang == self.language:
75
+ return True
76
+ from aipriceaction.settings import settings
77
+ if not settings.openai_api_key:
78
+ self.agent = None
79
+ return False
80
+ from .agents import AgentSession, AgentConfig
81
+ self.agent = AgentSession(AgentConfig(lang=self.language))
82
+ self._agent_lang = self.language
83
+ return True
84
+
72
85
  def compose(self) -> ComposeResult:
73
86
  yield Header(show_clock=True)
74
87
  with TabbedContent(initial="chat"):
@@ -216,7 +216,8 @@ class ChatTab(Vertical):
216
216
  )
217
217
  elif cmd == "/clear":
218
218
  log.clear()
219
- self.app.agent.clear_history()
219
+ if self.app.agent is not None:
220
+ self.app.agent.clear_history()
220
221
  elif cmd == "/exit":
221
222
  self.app.exit()
222
223
  elif cmd == "/analyze":
@@ -322,6 +323,13 @@ class ChatTab(Vertical):
322
323
  """Build context and stream AI analysis for a ticker."""
323
324
  log = self.query_one("#chat-log", RichLog)
324
325
  try:
326
+ if not self.app._ensure_agent():
327
+ log.write(
328
+ "[bold yellow]API key not configured.[/bold yellow]\n"
329
+ "Set it in the Settings tab or run [bold]aipa setup[/bold]."
330
+ )
331
+ return
332
+
325
333
  builder = self.app.builder
326
334
 
327
335
  # Build context without system prompt (agent has it already)
@@ -389,6 +397,12 @@ class ChatTab(Vertical):
389
397
  async def _run_agent_chat(self, message: str) -> None:
390
398
  """Stream an agent response into the chat log."""
391
399
  log = self.query_one("#chat-log", RichLog)
400
+ if not self.app._ensure_agent():
401
+ log.write(
402
+ "[bold yellow]API key not configured.[/bold yellow]\n"
403
+ "Set it in the Settings tab or run [bold]aipa setup[/bold]."
404
+ )
405
+ return
392
406
  try:
393
407
  await stream_agent_to_log(
394
408
  log,
@@ -3,7 +3,18 @@
3
3
  import argparse
4
4
 
5
5
 
6
+ def _ensure_setup() -> None:
7
+ """Run interactive setup if setup_done is not set."""
8
+ from .user_settings import load_settings
9
+ if not load_settings().get("setup_done"):
10
+ from .cli_setup import cmd_setup
11
+ cmd_setup()
12
+
13
+
6
14
  def run():
15
+ from .user_settings import apply_settings_to_env
16
+ apply_settings_to_env()
17
+
7
18
  parser = argparse.ArgumentParser(prog="aipa", description="AIPriceAction terminal")
8
19
  sub = parser.add_subparsers(dest="command")
9
20
 
@@ -44,15 +55,24 @@ def run():
44
55
  p_deep.add_argument("--output", default=None, help="Save final report to file")
45
56
  p_deep.add_argument("--lang", default=None, choices=["en", "vn"], help="Override language")
46
57
 
58
+ # aipa setup
59
+ sub.add_parser("setup", help="Interactive first-run setup")
60
+
47
61
  args = parser.parse_args()
48
62
 
49
- if args.command == "analyze":
63
+ if args.command == "setup":
64
+ from .cli_setup import cmd_setup
65
+ cmd_setup()
66
+ elif args.command == "analyze":
67
+ if not getattr(args, "context_only", False) and not getattr(args, "questions", False):
68
+ _ensure_setup()
50
69
  from .cli_commands import cmd_analyze
51
70
  cmd_analyze(args)
52
71
  elif args.command == "get-ohlcv-data":
53
72
  from .cli_commands import cmd_get_ohlcv
54
73
  cmd_get_ohlcv(args)
55
74
  elif args.command == "deep-research":
75
+ _ensure_setup()
56
76
  from .cli_commands import cmd_deep_research
57
77
  cmd_deep_research(
58
78
  question=" ".join(args.question) if args.question else "",
@@ -61,5 +81,6 @@ def run():
61
81
  lang=args.lang,
62
82
  )
63
83
  else:
84
+ _ensure_setup()
64
85
  from .app import main
65
86
  main()
@@ -0,0 +1,71 @@
1
+ """Interactive first-run setup for aipa CLI (plain terminal, no TUI)."""
2
+
3
+ from .user_settings import load_settings, save_settings
4
+
5
+
6
+ _DEFAULT_BASE_URL = "https://openrouter.ai/api/v1"
7
+ _DEFAULT_MODEL = "openai/gpt-oss-120b:free"
8
+
9
+
10
+ def cmd_setup() -> None:
11
+ """Run interactive setup and save settings."""
12
+ current = load_settings()
13
+
14
+ print("AIPriceAction Terminal Setup")
15
+ print("=" * 30)
16
+
17
+ # Language
18
+ lang = _prompt(
19
+ "Language (en/vn)",
20
+ current.get("language", "en"),
21
+ )
22
+ if lang not in ("en", "vn"):
23
+ print("Invalid language, defaulting to 'en'.")
24
+ lang = "en"
25
+
26
+ # Reference ticker
27
+ ticker = _prompt(
28
+ "Reference ticker",
29
+ current.get("ticker", "VNINDEX"),
30
+ )
31
+
32
+ # API key (optional)
33
+ api_key = _prompt(
34
+ "API key (press Enter to skip)",
35
+ current.get("api_key", ""),
36
+ )
37
+
38
+ # Base URL
39
+ base_url = _prompt(
40
+ "Base URL",
41
+ current.get("openai_base_url") or _DEFAULT_BASE_URL,
42
+ )
43
+
44
+ # Model
45
+ model = _prompt(
46
+ "Model",
47
+ current.get("openai_model") or _DEFAULT_MODEL,
48
+ )
49
+
50
+ data = {
51
+ "ticker": ticker.upper(),
52
+ "interval": current.get("interval", "1D"),
53
+ "language": lang,
54
+ "api_key": api_key,
55
+ "openai_base_url": base_url,
56
+ "openai_model": model,
57
+ "setup_done": True,
58
+ }
59
+
60
+ save_settings(data)
61
+ print("Setup complete.")
62
+
63
+
64
+ def _prompt(label: str, default: str) -> str:
65
+ """Prompt user with current default shown in brackets."""
66
+ hint = f" [{default}]" if default else ""
67
+ try:
68
+ value = input(f"{label}{hint}: ").strip()
69
+ except EOFError:
70
+ return default
71
+ return value if value else default
@@ -151,8 +151,13 @@ class SettingsTab(Vertical):
151
151
  self.app.language = language
152
152
  from aipriceaction import AIContextBuilder
153
153
  self.app.builder = AIContextBuilder(lang=language)
154
- from .agents import AgentSession, AgentConfig
155
- self.app.agent = AgentSession(AgentConfig(lang=language))
154
+ try:
155
+ from .agents import AgentSession, AgentConfig
156
+ self.app.agent = AgentSession(AgentConfig(lang=language))
157
+ self.app._agent_lang = language
158
+ except Exception:
159
+ self.app.agent = None
160
+ self.app._agent_lang = None
156
161
 
157
162
  data: dict = {
158
163
  "ticker": ticker,
@@ -1,6 +1,7 @@
1
1
  """Persistent user settings stored in ~/.aipriceaction/settings.json."""
2
2
 
3
3
  import json
4
+ import os
4
5
  from pathlib import Path
5
6
 
6
7
  _CONFIG_DIR = Path.home() / ".aipriceaction"
@@ -13,6 +14,7 @@ _DEFAULTS = {
13
14
  "api_key": "",
14
15
  "openai_base_url": "",
15
16
  "openai_model": "",
17
+ "setup_done": False,
16
18
  }
17
19
 
18
20
 
@@ -28,3 +30,21 @@ def save_settings(data: dict) -> None:
28
30
  """Persist settings to disk, creating the config directory if needed."""
29
31
  _CONFIG_DIR.mkdir(parents=True, exist_ok=True)
30
32
  _SETTINGS_FILE.write_text(json.dumps(data, indent=2))
33
+
34
+
35
+ def apply_settings_to_env() -> None:
36
+ """Seed environment variables from settings.json.
37
+
38
+ Uses ``os.environ.setdefault`` so that existing env vars always win.
39
+ Must be called before any SDK import that reads env vars.
40
+ """
41
+ settings = load_settings()
42
+ _mapping = {
43
+ "OPENAI_API_KEY": "api_key",
44
+ "OPENAI_BASE_URL": "openai_base_url",
45
+ "OPENAI_MODEL": "openai_model",
46
+ }
47
+ for env_key, settings_key in _mapping.items():
48
+ value = settings.get(settings_key, "")
49
+ if value:
50
+ os.environ.setdefault(env_key, value)
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes