kitecli 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.
- cli/__init__.py +11 -0
- cli/advisor.py +283 -0
- cli/api_client.py +421 -0
- cli/config.py +81 -0
- cli/display.py +395 -0
- cli/kite_manager.py +1042 -0
- cli/live_session.py +3841 -0
- cli/main.py +351 -0
- cli/nli.py +140 -0
- cli/recorder.py +321 -0
- cli/telegram_bot.py +1074 -0
- kitecli-0.1.0.dist-info/METADATA +259 -0
- kitecli-0.1.0.dist-info/RECORD +16 -0
- kitecli-0.1.0.dist-info/WHEEL +5 -0
- kitecli-0.1.0.dist-info/entry_points.txt +2 -0
- kitecli-0.1.0.dist-info/top_level.txt +1 -0
cli/main.py
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
"""
|
|
2
|
+
KiteCLI โ Multi-account Kite Connect positions viewer.
|
|
3
|
+
|
|
4
|
+
Entry-point module that defines all ``kcli`` commands using Typer.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
import typer
|
|
10
|
+
|
|
11
|
+
import asyncio
|
|
12
|
+
|
|
13
|
+
from cli.api_client import KCLIClient, KCLIClientError
|
|
14
|
+
from cli.config import CONFIG_FILE, create_default_config, load_config
|
|
15
|
+
from cli.display import (
|
|
16
|
+
console,
|
|
17
|
+
display_banner,
|
|
18
|
+
display_error,
|
|
19
|
+
display_info,
|
|
20
|
+
display_login_urls,
|
|
21
|
+
display_positions,
|
|
22
|
+
display_status,
|
|
23
|
+
display_success,
|
|
24
|
+
)
|
|
25
|
+
from cli.live_session import KCLILiveSession
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
app = typer.Typer(
|
|
29
|
+
name="kcli",
|
|
30
|
+
help="[bold cyan]Kite Connect CLI[/bold cyan] โ Multi-account trading positions viewer ๐ช",
|
|
31
|
+
no_args_is_help=True,
|
|
32
|
+
rich_markup_mode="rich",
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# โโ helpers โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
37
|
+
|
|
38
|
+
def _load_config_or_exit() -> dict:
|
|
39
|
+
"""Load config and exit with a helpful message if missing."""
|
|
40
|
+
config = load_config()
|
|
41
|
+
if config is None:
|
|
42
|
+
display_error(
|
|
43
|
+
f"Config file not found at {CONFIG_FILE}\n"
|
|
44
|
+
" Run [bold]kcli config --init[/bold] to create one."
|
|
45
|
+
)
|
|
46
|
+
raise typer.Exit(code=1)
|
|
47
|
+
return config
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _build_client(config: dict) -> KCLIClient:
|
|
51
|
+
"""Construct a local KCLIClient from the loaded config."""
|
|
52
|
+
accounts = config.get("accounts", [])
|
|
53
|
+
return KCLIClient(accounts)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _ensure_accounts_initialized(client: KCLIClient, config: dict) -> None:
|
|
57
|
+
"""Show per-account init status; in local mode accounts are init'd in KCLIClient.__init__."""
|
|
58
|
+
status_resp = client.get_status()
|
|
59
|
+
for acct in status_resp.get("accounts", []):
|
|
60
|
+
name = acct.get("name", "Account")
|
|
61
|
+
if acct.get("authenticated"):
|
|
62
|
+
display_success(f"{name}: session active โ")
|
|
63
|
+
else:
|
|
64
|
+
display_info(f"{name}: not authenticated โ run [bold]kcli init[/bold]")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _mask_secret(value: str, visible: int = 4) -> str:
|
|
68
|
+
"""Mask a secret string, showing only the last ``visible`` chars."""
|
|
69
|
+
if len(value) <= visible:
|
|
70
|
+
return "*" * len(value)
|
|
71
|
+
return "*" * (len(value) - visible) + value[-visible:]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# โโ commands โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
75
|
+
|
|
76
|
+
@app.command()
|
|
77
|
+
def init() -> None:
|
|
78
|
+
"""[bold]Initialise accounts[/bold] โ authenticate with Kite Connect.
|
|
79
|
+
|
|
80
|
+
Sends account credentials to the server, shows login URLs,
|
|
81
|
+
then completes the auth callback for each account.
|
|
82
|
+
"""
|
|
83
|
+
display_banner()
|
|
84
|
+
|
|
85
|
+
config = _load_config_or_exit()
|
|
86
|
+
accounts = config.get("accounts", [])
|
|
87
|
+
if not accounts:
|
|
88
|
+
display_error("No accounts configured. Edit your config file to add accounts.")
|
|
89
|
+
raise typer.Exit(code=1)
|
|
90
|
+
client = _build_client(config)
|
|
91
|
+
console.print()
|
|
92
|
+
|
|
93
|
+
accounts = config.get("accounts", [])
|
|
94
|
+
if not accounts:
|
|
95
|
+
display_error("No accounts configured. Edit your config file to add accounts.")
|
|
96
|
+
raise typer.Exit(code=1)
|
|
97
|
+
|
|
98
|
+
# Init accounts (auto-login is attempted)
|
|
99
|
+
try:
|
|
100
|
+
display_info("Initialising accountsโฆ")
|
|
101
|
+
result = client.init_accounts(accounts)
|
|
102
|
+
console.print()
|
|
103
|
+
except KCLIClientError as exc:
|
|
104
|
+
display_error(str(exc))
|
|
105
|
+
raise typer.Exit(code=1)
|
|
106
|
+
|
|
107
|
+
login_accounts = result.get("accounts", [])
|
|
108
|
+
|
|
109
|
+
# Separate auto-logged-in accounts from those needing manual login
|
|
110
|
+
auto_logged = [a for a in login_accounts if a.get("auto_logged_in")]
|
|
111
|
+
manual_needed = [a for a in login_accounts if not a.get("auto_logged_in")]
|
|
112
|
+
|
|
113
|
+
# Show auto-login results
|
|
114
|
+
for acct in auto_logged:
|
|
115
|
+
display_success(f"{acct.get('name', 'Account')}: Auto-login successful! โ")
|
|
116
|
+
console.print()
|
|
117
|
+
|
|
118
|
+
# Handle accounts that need manual login
|
|
119
|
+
if manual_needed:
|
|
120
|
+
display_login_urls(manual_needed)
|
|
121
|
+
console.print()
|
|
122
|
+
|
|
123
|
+
# Show fallback messages for failed auto-logins
|
|
124
|
+
for acct in manual_needed:
|
|
125
|
+
msg = acct.get("message", "")
|
|
126
|
+
if msg:
|
|
127
|
+
display_info(f"{acct.get('name', 'Account')}: {msg}")
|
|
128
|
+
|
|
129
|
+
console.print(
|
|
130
|
+
" [bold yellow]After logging in, enter the request_token from the "
|
|
131
|
+
"redirect URL for each account.[/bold yellow]\n"
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
for acct in manual_needed:
|
|
135
|
+
name = acct.get("name", "Account")
|
|
136
|
+
api_key = acct.get("api_key", "")
|
|
137
|
+
|
|
138
|
+
request_token = typer.prompt(
|
|
139
|
+
f" ๐ request_token for {name}",
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
try:
|
|
143
|
+
resp = client.complete_callback(api_key, request_token.strip())
|
|
144
|
+
if resp.get("status") == "error":
|
|
145
|
+
display_error(f"{name}: {resp.get('message', 'Callback failed')}")
|
|
146
|
+
else:
|
|
147
|
+
display_success(f"{name}: Authenticated successfully! โ")
|
|
148
|
+
except KCLIClientError as exc:
|
|
149
|
+
display_error(f"{name}: {exc}")
|
|
150
|
+
|
|
151
|
+
console.print()
|
|
152
|
+
|
|
153
|
+
display_success("Initialisation complete. Run [bold]kcli positions[/bold] to view positions.")
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@app.command()
|
|
157
|
+
def positions() -> None:
|
|
158
|
+
"""[bold]View positions[/bold] โ fetch and display open positions across all accounts."""
|
|
159
|
+
display_banner()
|
|
160
|
+
|
|
161
|
+
config = _load_config_or_exit()
|
|
162
|
+
client = _build_client(config)
|
|
163
|
+
|
|
164
|
+
accounts = config.get("accounts", [])
|
|
165
|
+
api_keys = [acct.get("api_key", "") for acct in accounts]
|
|
166
|
+
|
|
167
|
+
try:
|
|
168
|
+
display_info("Fetching positionsโฆ")
|
|
169
|
+
console.print()
|
|
170
|
+
result = client.get_positions(api_keys)
|
|
171
|
+
except KCLIClientError as exc:
|
|
172
|
+
display_error(str(exc))
|
|
173
|
+
raise typer.Exit(code=1)
|
|
174
|
+
|
|
175
|
+
accounts_data = result.get("accounts", [])
|
|
176
|
+
if not accounts_data:
|
|
177
|
+
display_info("No position data returned.")
|
|
178
|
+
raise typer.Exit()
|
|
179
|
+
|
|
180
|
+
display_positions(accounts_data)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@app.command()
|
|
184
|
+
def status() -> None:
|
|
185
|
+
"""[bold]Account status[/bold] โ check authentication state of all accounts."""
|
|
186
|
+
display_banner()
|
|
187
|
+
|
|
188
|
+
config = _load_config_or_exit()
|
|
189
|
+
client = _build_client(config)
|
|
190
|
+
result = client.get_status()
|
|
191
|
+
|
|
192
|
+
status_accounts = result.get("accounts", [])
|
|
193
|
+
if not status_accounts:
|
|
194
|
+
display_info("No account status returned.")
|
|
195
|
+
raise typer.Exit()
|
|
196
|
+
|
|
197
|
+
display_status(status_accounts)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@app.command()
|
|
201
|
+
def live() -> None:
|
|
202
|
+
"""[bold]Live dashboard[/bold] โ Interactive positions monitor and order terminal."""
|
|
203
|
+
config = _load_config_or_exit()
|
|
204
|
+
accounts = config.get("accounts", [])
|
|
205
|
+
if not accounts:
|
|
206
|
+
display_error("No accounts configured. Edit your config file to add accounts.")
|
|
207
|
+
raise typer.Exit(code=1)
|
|
208
|
+
client = _build_client(config)
|
|
209
|
+
|
|
210
|
+
# Launch interactive session
|
|
211
|
+
session = KCLILiveSession(client, accounts)
|
|
212
|
+
try:
|
|
213
|
+
asyncio.run(session.run())
|
|
214
|
+
except KeyboardInterrupt:
|
|
215
|
+
pass
|
|
216
|
+
except Exception as exc:
|
|
217
|
+
display_error(f"TUI Error: {exc}")
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@app.command("config")
|
|
221
|
+
def config_cmd(
|
|
222
|
+
init: bool = typer.Option(False, "--init", help="Create a default config file."),
|
|
223
|
+
show: bool = typer.Option(False, "--show", help="Display the current configuration."),
|
|
224
|
+
path: bool = typer.Option(False, "--path", help="Print the config file path."),
|
|
225
|
+
) -> None:
|
|
226
|
+
"""[bold]Configuration[/bold] โ manage the kcli config file."""
|
|
227
|
+
display_banner()
|
|
228
|
+
|
|
229
|
+
if path:
|
|
230
|
+
console.print(f" ๐ Config file: [bold]{CONFIG_FILE}[/bold]")
|
|
231
|
+
return
|
|
232
|
+
|
|
233
|
+
if init:
|
|
234
|
+
if CONFIG_FILE.exists():
|
|
235
|
+
overwrite = typer.confirm(
|
|
236
|
+
f" Config already exists at {CONFIG_FILE}. Overwrite?",
|
|
237
|
+
default=False,
|
|
238
|
+
)
|
|
239
|
+
if not overwrite:
|
|
240
|
+
display_info("Aborted. Existing config left unchanged.")
|
|
241
|
+
return
|
|
242
|
+
|
|
243
|
+
create_default_config()
|
|
244
|
+
display_success(f"Default config created at {CONFIG_FILE}")
|
|
245
|
+
display_info(
|
|
246
|
+
"Edit the config file with your Kite Connect credentials:\n"
|
|
247
|
+
f" [bold]{CONFIG_FILE}[/bold]"
|
|
248
|
+
)
|
|
249
|
+
return
|
|
250
|
+
|
|
251
|
+
# Default behaviour: --show (also the fallback when no flags given)
|
|
252
|
+
config = load_config()
|
|
253
|
+
if config is None:
|
|
254
|
+
display_error(
|
|
255
|
+
f"No config found at {CONFIG_FILE}\n"
|
|
256
|
+
" Run [bold]kcli config --init[/bold] to create one."
|
|
257
|
+
)
|
|
258
|
+
raise typer.Exit(code=1)
|
|
259
|
+
|
|
260
|
+
# Pretty-print config with masked secrets
|
|
261
|
+
console.print()
|
|
262
|
+
console.print(" [bold cyan]Accounts[/bold cyan]")
|
|
263
|
+
for i, acct in enumerate(config.get("accounts", []), start=1):
|
|
264
|
+
console.print(f" [bold]{i}. {acct.get('name', 'Account')}[/bold]")
|
|
265
|
+
console.print(f" api_key : {acct.get('api_key', 'N/A')}")
|
|
266
|
+
console.print(
|
|
267
|
+
f" api_secret : [dim]{_mask_secret(acct.get('api_secret', ''))}[/dim]"
|
|
268
|
+
)
|
|
269
|
+
if acct.get("user_id"):
|
|
270
|
+
console.print(f" user_id : {acct.get('user_id', 'N/A')}")
|
|
271
|
+
if acct.get("password"):
|
|
272
|
+
console.print(
|
|
273
|
+
f" password : [dim]{_mask_secret(acct.get('password', ''))}[/dim]"
|
|
274
|
+
)
|
|
275
|
+
if acct.get("totp_secret"):
|
|
276
|
+
console.print(
|
|
277
|
+
f" totp_secret: [dim]{_mask_secret(acct.get('totp_secret', ''))}[/dim]"
|
|
278
|
+
)
|
|
279
|
+
console.print()
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@app.command("bot")
|
|
283
|
+
def bot_cmd(
|
|
284
|
+
token: Optional[str] = typer.Option(None, "--token", help="Telegram Bot Token"),
|
|
285
|
+
chat_id: Optional[int] = typer.Option(None, "--chat-id", help="Authorized Telegram Chat ID"),
|
|
286
|
+
) -> None:
|
|
287
|
+
"""[bold]Run Telegram bot[/bold] โ run the bot in polling mode to manage positions and orders."""
|
|
288
|
+
display_banner()
|
|
289
|
+
|
|
290
|
+
config = _load_config_or_exit()
|
|
291
|
+
|
|
292
|
+
import os
|
|
293
|
+
tg_config = config.get("telegram", {})
|
|
294
|
+
resolved_token = token or tg_config.get("bot_token") or os.environ.get("TELEGRAM_BOT_TOKEN")
|
|
295
|
+
resolved_chat_id = chat_id or tg_config.get("chat_id") or os.environ.get("TELEGRAM_CHAT_ID")
|
|
296
|
+
|
|
297
|
+
if not resolved_token:
|
|
298
|
+
display_error(
|
|
299
|
+
"Telegram Bot Token not provided.\n"
|
|
300
|
+
" Please configure it in ~/.kcli/config.yaml under a `telegram:` section, or pass it via `--token`."
|
|
301
|
+
)
|
|
302
|
+
raise typer.Exit(code=1)
|
|
303
|
+
|
|
304
|
+
if not resolved_chat_id:
|
|
305
|
+
display_error(
|
|
306
|
+
"Authorized Telegram Chat ID not provided.\n"
|
|
307
|
+
" Please configure it in ~/.kcli/config.yaml under `telegram: chat_id`, pass it via `--chat-id`, or set the `TELEGRAM_CHAT_ID` environment variable."
|
|
308
|
+
)
|
|
309
|
+
raise typer.Exit(code=1)
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
resolved_chat_id = int(resolved_chat_id)
|
|
313
|
+
except ValueError:
|
|
314
|
+
display_error("Chat ID must be a numeric value.")
|
|
315
|
+
raise typer.Exit(code=1)
|
|
316
|
+
|
|
317
|
+
client = _build_client(config)
|
|
318
|
+
|
|
319
|
+
# Initialize connection diagnostics
|
|
320
|
+
display_info("Initializing client session...")
|
|
321
|
+
_ensure_accounts_initialized(client, config)
|
|
322
|
+
|
|
323
|
+
from cli.telegram_bot import KCLITelegramBot
|
|
324
|
+
bot = KCLITelegramBot(client=client, token=resolved_token, chat_id=resolved_chat_id)
|
|
325
|
+
|
|
326
|
+
display_success(f"Starting Telegram Bot for Chat ID: {resolved_chat_id}... (Press Ctrl+C to exit)")
|
|
327
|
+
|
|
328
|
+
loop = asyncio.get_event_loop()
|
|
329
|
+
try:
|
|
330
|
+
loop.run_until_complete(bot.start())
|
|
331
|
+
while True:
|
|
332
|
+
loop.run_until_complete(asyncio.sleep(1))
|
|
333
|
+
except KeyboardInterrupt:
|
|
334
|
+
display_info("\nStopping bot...")
|
|
335
|
+
finally:
|
|
336
|
+
try:
|
|
337
|
+
loop.run_until_complete(bot.stop())
|
|
338
|
+
except Exception as e:
|
|
339
|
+
logger.error(f"Error during bot stop: {e}")
|
|
340
|
+
display_success("Bot stopped cleanly.")
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# โโ entry point โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
|
|
344
|
+
|
|
345
|
+
def main() -> None:
|
|
346
|
+
"""CLI entry point."""
|
|
347
|
+
app()
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
if __name__ == "__main__":
|
|
351
|
+
main()
|
cli/nli.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import socket
|
|
5
|
+
import requests
|
|
6
|
+
import urllib3.util.connection as connection
|
|
7
|
+
from typing import Any, Dict, List
|
|
8
|
+
|
|
9
|
+
# Force IPv4 resolution globally to prevent macOS IPv6 lookup timeouts/delays
|
|
10
|
+
def allowed_gai_family():
|
|
11
|
+
return socket.AF_INET
|
|
12
|
+
|
|
13
|
+
connection.allowed_gai_family = allowed_gai_family
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
SYSTEM_INSTRUCTION = """
|
|
18
|
+
You are the Natural Language Interface (NLI) parsing engine for kcli, a multi-account Zerodha Kite option trading terminal.
|
|
19
|
+
Your task is to translate user natural language requests into exact kcli CLI commands.
|
|
20
|
+
|
|
21
|
+
Available Command Syntax:
|
|
22
|
+
1. Switch account context:
|
|
23
|
+
- Syntax: `account <name>` (e.g. `account ZK8719`, `account SS1009`)
|
|
24
|
+
2. Exit/square off specific position:
|
|
25
|
+
- Syntax: `exit <symbol>` (e.g. `exit NIFTY26JUN22200PE`)
|
|
26
|
+
3. Exit all positions:
|
|
27
|
+
- Syntax: `exit all` (exits all open positions across targeted accounts)
|
|
28
|
+
4. Place orders:
|
|
29
|
+
- Syntax: `buy <symbol> <quantity> [price] [product]` or `sell <symbol> <quantity> [price] [product]`
|
|
30
|
+
- Quantity can be in lots using the 'L' suffix (e.g. `27L` for 27 lots, `2L` for 2 lots).
|
|
31
|
+
- Product can be specified at the end: `MIS` or `NRML`. Default is MIS.
|
|
32
|
+
- Price can be optionally specified as a raw numeric decimal/float (e.g., `1.4` or `125.5`).
|
|
33
|
+
- **CRITICAL**: Do NOT prefix the price with the `@` symbol. (For example, `buy NIFTY26JUN22300PE 50 @1.4` is WRONG; use `buy NIFTY26JUN22300PE 50 1.4` instead).
|
|
34
|
+
- Example 1 (Market Order): `sell NIFTY26JUN24000CE 27L`
|
|
35
|
+
- Example 2 (Limit Order): `buy NIFTY26JUN22300PE 1L 1.4`
|
|
36
|
+
5. Show Option Chain:
|
|
37
|
+
- Syntax: `oc <UNDERLYING> [week <N>]` (e.g. `oc NIFTY`, `oc NIFTY week 1`)
|
|
38
|
+
6. Sync and Utilities:
|
|
39
|
+
- Syntax: `refresh` (refresh data)
|
|
40
|
+
- Syntax: `clear` (clear logs)
|
|
41
|
+
|
|
42
|
+
Command Chaining:
|
|
43
|
+
Multiple commands can be chained together using ` && `.
|
|
44
|
+
- Example: `account ZK8719 && exit all`
|
|
45
|
+
- Example: `account VJR419 && sell NIFTY26JUN24000CE 27L && sell NIFTY26JUN22000PE 27L`
|
|
46
|
+
|
|
47
|
+
Resolving Exits and Replication Orders via Open Positions:
|
|
48
|
+
- You must always inspect the provided `open_positions` list.
|
|
49
|
+
- If the user asks to "exit", "close", "replicate", or "sell" weekly options:
|
|
50
|
+
1. First, search the provided `open_positions` list across all accounts. If any account already holds active positions for the matching expiry or category, extract their exact symbols (`tradingsymbol`) and use those same symbols to formulate the commands. This ensures you replicate the exact same strikes and symbols across accounts.
|
|
51
|
+
2. If no accounts have matching positions open, look up the target option symbols in the provided `available_options` list matching the target expiry and strikes.
|
|
52
|
+
- Do NOT invent or estimate option symbols. Always use either `open_positions` or the fallback `available_options` list.
|
|
53
|
+
|
|
54
|
+
Context Provided:
|
|
55
|
+
We will pass you the user's input, the selected account context, available account names, the nearest weekly expiries, the list of available option contracts near the spot price, and the list of active open positions across all accounts. Use this context to resolve abbreviations (e.g. "zk" -> "ZK8719").
|
|
56
|
+
|
|
57
|
+
OUTPUT FORMAT:
|
|
58
|
+
You MUST return a JSON object with the following fields:
|
|
59
|
+
- `command`: The exact matched kcli CLI command string. If the request cannot be translated, return an empty string.
|
|
60
|
+
- `confidence`: Confidence score float between 0.0 and 1.0.
|
|
61
|
+
- `explanation`: A short description explaining what the command will do.
|
|
62
|
+
|
|
63
|
+
Strict Rules:
|
|
64
|
+
- Do NOT wrap your output in markdown code blocks like ```json ... ```. Output raw JSON only.
|
|
65
|
+
- Generate only valid kcli commands as defined above. Do not invent any new commands.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
async def parse_natural_language(
|
|
69
|
+
api_key: str,
|
|
70
|
+
user_input: str,
|
|
71
|
+
selected_account: str | None,
|
|
72
|
+
accounts_list: List[str],
|
|
73
|
+
open_positions: List[Dict[str, Any]],
|
|
74
|
+
nifty_spot: float | None,
|
|
75
|
+
nearest_expiries: Dict[str, str] | None = None,
|
|
76
|
+
available_options: List[Dict[str, Any]] | None = None,
|
|
77
|
+
model: str = "gemini-2.5-flash"
|
|
78
|
+
) -> Dict[str, Any]:
|
|
79
|
+
"""
|
|
80
|
+
Send natural language request to Gemini API and get structured JSON response.
|
|
81
|
+
"""
|
|
82
|
+
url = f"https://generativelanguage.googleapis.com/v1beta/models/{model}:generateContent?key={api_key}"
|
|
83
|
+
headers = {"Content-Type": "application/json"}
|
|
84
|
+
|
|
85
|
+
# Formulate context payload for the prompt using local state and option database fallback
|
|
86
|
+
context = {
|
|
87
|
+
"user_input": user_input,
|
|
88
|
+
"current_context": {
|
|
89
|
+
"selected_account": selected_account,
|
|
90
|
+
"nifty_spot": nifty_spot,
|
|
91
|
+
"available_accounts": accounts_list,
|
|
92
|
+
"nearest_expiry_by_underlying": nearest_expiries,
|
|
93
|
+
"available_options": available_options,
|
|
94
|
+
"open_positions": [
|
|
95
|
+
{
|
|
96
|
+
"account": pos.get("account_name"),
|
|
97
|
+
"symbol": pos.get("tradingsymbol"),
|
|
98
|
+
"quantity": pos.get("quantity"),
|
|
99
|
+
"pnl": pos.get("pnl")
|
|
100
|
+
}
|
|
101
|
+
for pos in open_positions
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
prompt = json.dumps(context, indent=2)
|
|
107
|
+
|
|
108
|
+
payload = {
|
|
109
|
+
"contents": [{"parts": [{"text": prompt}]}],
|
|
110
|
+
"systemInstruction": {"parts": [{"text": SYSTEM_INSTRUCTION}]},
|
|
111
|
+
"generationConfig": {
|
|
112
|
+
"responseMimeType": "application/json"
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
resp = await asyncio.to_thread(
|
|
118
|
+
requests.post,
|
|
119
|
+
url,
|
|
120
|
+
headers=headers,
|
|
121
|
+
json=payload,
|
|
122
|
+
proxies={"http": None, "https": None},
|
|
123
|
+
timeout=15
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
if resp.status_code == 200:
|
|
127
|
+
result = resp.json()
|
|
128
|
+
try:
|
|
129
|
+
text = result["candidates"][0]["content"]["parts"][0]["text"]
|
|
130
|
+
parsed = json.loads(text.strip())
|
|
131
|
+
return parsed
|
|
132
|
+
except (KeyError, IndexError, json.JSONDecodeError) as exc:
|
|
133
|
+
logger.error("Failed to parse Gemini JSON output: %s. Raw text: %s", exc, text if 'text' in locals() else "None")
|
|
134
|
+
return {"command": "", "confidence": 0.0, "explanation": f"API parsing error: {exc}"}
|
|
135
|
+
else:
|
|
136
|
+
logger.error("Gemini API error (HTTP %s): %s", resp.status_code, resp.text)
|
|
137
|
+
return {"command": "", "confidence": 0.0, "explanation": f"Gemini API returned status {resp.status_code}"}
|
|
138
|
+
except Exception as exc:
|
|
139
|
+
logger.error("Gemini API request failed: %s", exc)
|
|
140
|
+
return {"command": "", "confidence": 0.0, "explanation": f"Request failed: {exc}"}
|