asky-cli 0.1.7__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.
- asky/__init__.py +7 -0
- asky/__main__.py +6 -0
- asky/banner.py +123 -0
- asky/cli/__init__.py +11 -0
- asky/cli/chat.py +172 -0
- asky/cli/history.py +98 -0
- asky/cli/main.py +230 -0
- asky/cli/prompts.py +20 -0
- asky/cli/utils.py +100 -0
- asky/config/__init__.py +103 -0
- asky/config/loader.py +121 -0
- asky/config.toml +261 -0
- asky/core/__init__.py +18 -0
- asky/core/api_client.py +164 -0
- asky/core/engine.py +359 -0
- asky/core/page_crawler.py +150 -0
- asky/core/prompts.py +75 -0
- asky/core/registry.py +77 -0
- asky/email_sender.py +149 -0
- asky/html.py +80 -0
- asky/llm.py +4 -0
- asky/logger.py +51 -0
- asky/rendering.py +37 -0
- asky/storage/__init__.py +52 -0
- asky/storage/interface.py +88 -0
- asky/storage/sqlite.py +194 -0
- asky/summarization.py +94 -0
- asky/template.html +230 -0
- asky/tools.py +236 -0
- asky_cli-0.1.7.dist-info/METADATA +293 -0
- asky_cli-0.1.7.dist-info/RECORD +33 -0
- asky_cli-0.1.7.dist-info/WHEEL +4 -0
- asky_cli-0.1.7.dist-info/entry_points.txt +3 -0
asky/__init__.py
ADDED
asky/__main__.py
ADDED
asky/banner.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Cute Terminal Icon - ASCII Art
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
from rich.text import Text
|
|
8
|
+
from rich.panel import Panel
|
|
9
|
+
from rich.table import Table
|
|
10
|
+
from rich import box
|
|
11
|
+
|
|
12
|
+
# Color shortcuts for templates
|
|
13
|
+
G = "bold #39ff14" # neon green (eyes)
|
|
14
|
+
P = "#ff8fa3" # pink (blush)
|
|
15
|
+
D = "dim" # dim (border)
|
|
16
|
+
N = "#39ff14" # nose (not bold = dimmer)
|
|
17
|
+
|
|
18
|
+
# Metallic shiny effect colors
|
|
19
|
+
M1 = "bold #ffffff" # bright highlight
|
|
20
|
+
M2 = "bold #c0c0c0" # silver
|
|
21
|
+
M3 = "#a0a0a0" # medium gray
|
|
22
|
+
M4 = "#707070" # shadow
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def display(lines):
|
|
26
|
+
"""Display lines with markup"""
|
|
27
|
+
console = Console()
|
|
28
|
+
console.print()
|
|
29
|
+
for line in lines:
|
|
30
|
+
console.print(Text.from_markup(line))
|
|
31
|
+
console.print()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def mini():
|
|
35
|
+
"""Mini version"""
|
|
36
|
+
display(
|
|
37
|
+
[
|
|
38
|
+
f"[{G}] ∩ ∩[/{G}] [{M1}]a[/{M1}]",
|
|
39
|
+
f"[{D}] ╭┴─────┴╮[/{D}] [{M2}]s[/{M2}]",
|
|
40
|
+
f"[{D}] │[/{D}] [{G}]▷[/{G}] [{D}][{N}]ω[/{N}][/{D}] [{G}]_[/{G}] [{D}]│[/{D}] [{M3}]k[/{M3}]",
|
|
41
|
+
f"[{D}] │[/{D}] [{P}]◠[/{P}] [{P}]◠[/{P}] [{D}]│[/{D}] [{M4}]y[/{M4}]",
|
|
42
|
+
f"[{D}] ╰───────╯[/{D}] ",
|
|
43
|
+
]
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def get_banner(
|
|
48
|
+
model_alias: str,
|
|
49
|
+
model_id: str,
|
|
50
|
+
sum_alias: str,
|
|
51
|
+
sum_id: str,
|
|
52
|
+
default_model: str,
|
|
53
|
+
search_provider: str,
|
|
54
|
+
model_ctx: int,
|
|
55
|
+
sum_ctx: int,
|
|
56
|
+
max_turns: int,
|
|
57
|
+
db_count: int,
|
|
58
|
+
) -> Panel:
|
|
59
|
+
"""Create a side-by-side banner with an icon and configuration info in two columns."""
|
|
60
|
+
icon_lines = [
|
|
61
|
+
f"[{G}] ∩ ∩[/{G}] [{M1}]a[/{M1}]",
|
|
62
|
+
f"[{D}] ╭┴─────┴╮[/{D}] [{M2}]s[/{M2}]",
|
|
63
|
+
f"[{D}] │[/{D}] [{G}]▷[/{G}] [{D}][{N}]ω[/{N}][/{D}] [{G}]_[/{G}] [{D}]│[/{D}] [{M3}]k[/{M3}]",
|
|
64
|
+
f"[{D}] │[/{D}] [{P}]◠[/{P}] [{P}]◠[/{P}] [{D}]│[/{D}] [{M4}]y[/{M4}]",
|
|
65
|
+
f"[{D}] ╰───────╯[/{D}] ",
|
|
66
|
+
]
|
|
67
|
+
icon_text = Text.from_markup("\n".join(icon_lines))
|
|
68
|
+
|
|
69
|
+
# --- Configuration Columns ---
|
|
70
|
+
col1 = Table.grid(padding=(0, 1))
|
|
71
|
+
col1.add_column(justify="left", style="bold cyan")
|
|
72
|
+
col1.add_column(justify="left")
|
|
73
|
+
col1.add_row(
|
|
74
|
+
" Main Model :", f" [white]{model_alias}[/white] ([dim]{model_id}[/dim])"
|
|
75
|
+
)
|
|
76
|
+
col1.add_row(" Summarizer :", f" [white]{sum_alias}[/white] ([dim]{sum_id}[/dim])")
|
|
77
|
+
col1.add_row(" Default :", f" [white]{default_model}[/white]")
|
|
78
|
+
|
|
79
|
+
col2 = Table.grid(padding=(0, 1))
|
|
80
|
+
col2.add_column(justify="left", style="bold cyan")
|
|
81
|
+
col2.add_column(justify="left")
|
|
82
|
+
col2.add_row(" Search :", f" [white]{search_provider}[/white]")
|
|
83
|
+
col2.add_row(
|
|
84
|
+
" Context :",
|
|
85
|
+
f" [white]{model_ctx:,}[/white]/[white]{sum_ctx:,}[/white] [dim]tokens[/dim]",
|
|
86
|
+
)
|
|
87
|
+
col2.add_row(
|
|
88
|
+
" System :",
|
|
89
|
+
f" [white]{max_turns}[/white] [dim]turns[/dim] | [white]{db_count}[/white] [dim]records[/dim]",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
info_layout = Table.grid(padding=(0, 3))
|
|
93
|
+
info_layout.add_column()
|
|
94
|
+
info_layout.add_column()
|
|
95
|
+
info_layout.add_row(col1, col2)
|
|
96
|
+
|
|
97
|
+
# --- Main Layout ---
|
|
98
|
+
layout_table = Table.grid(padding=(0, 2))
|
|
99
|
+
layout_table.add_column()
|
|
100
|
+
layout_table.add_column(ratio=1)
|
|
101
|
+
layout_table.add_row(icon_text, info_layout)
|
|
102
|
+
|
|
103
|
+
return Panel(layout_table, box=box.ROUNDED, border_style="dim", padding=(0, 1))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
if __name__ == "__main__":
|
|
107
|
+
mini()
|
|
108
|
+
# Test get_banner
|
|
109
|
+
console = Console()
|
|
110
|
+
console.print(
|
|
111
|
+
get_banner(
|
|
112
|
+
"gf",
|
|
113
|
+
"gemini-flash-latest",
|
|
114
|
+
"lfm",
|
|
115
|
+
"llama3",
|
|
116
|
+
"gf",
|
|
117
|
+
"searxng",
|
|
118
|
+
1000000,
|
|
119
|
+
4096,
|
|
120
|
+
20,
|
|
121
|
+
123,
|
|
122
|
+
)
|
|
123
|
+
)
|
asky/cli/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""CLI package for asky."""
|
|
2
|
+
|
|
3
|
+
from asky.cli.history import (
|
|
4
|
+
show_history_command as show_history,
|
|
5
|
+
print_answers_command as print_answers,
|
|
6
|
+
handle_cleanup_command as handle_cleanup,
|
|
7
|
+
)
|
|
8
|
+
from asky.cli.prompts import list_prompts_command as list_prompts
|
|
9
|
+
from asky.cli.chat import run_chat, load_context, build_messages
|
|
10
|
+
from asky.cli.utils import expand_query_text, print_config
|
|
11
|
+
from asky.cli.main import main, parse_args, handle_print_answer_implicit
|
asky/cli/chat.py
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""Chat implementation for asky CLI."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
import argparse
|
|
5
|
+
from typing import List, Dict, Any, Optional
|
|
6
|
+
|
|
7
|
+
from asky.config import (
|
|
8
|
+
MODELS,
|
|
9
|
+
SUMMARIZATION_MODEL,
|
|
10
|
+
QUERY_EXPANSION_MAX_DEPTH,
|
|
11
|
+
)
|
|
12
|
+
from asky.core import (
|
|
13
|
+
ConversationEngine,
|
|
14
|
+
create_default_tool_registry,
|
|
15
|
+
create_deep_dive_tool_registry,
|
|
16
|
+
UsageTracker,
|
|
17
|
+
construct_system_prompt,
|
|
18
|
+
generate_summaries,
|
|
19
|
+
)
|
|
20
|
+
from asky.storage import (
|
|
21
|
+
get_history,
|
|
22
|
+
get_interaction_context,
|
|
23
|
+
save_interaction,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def load_context(continue_ids: str, summarize: bool) -> Optional[str]:
|
|
28
|
+
"""Load context from previous interactions."""
|
|
29
|
+
try:
|
|
30
|
+
raw_ids = [x.strip() for x in continue_ids.split(",")]
|
|
31
|
+
resolved_ids = []
|
|
32
|
+
relative_indices = []
|
|
33
|
+
|
|
34
|
+
for raw_id in raw_ids:
|
|
35
|
+
if raw_id.startswith("~"):
|
|
36
|
+
try:
|
|
37
|
+
rel_val = int(raw_id[1:])
|
|
38
|
+
if rel_val < 1:
|
|
39
|
+
print(f"Error: Relative ID must be >= 1 (got {raw_id})")
|
|
40
|
+
return None
|
|
41
|
+
relative_indices.append(rel_val)
|
|
42
|
+
except ValueError:
|
|
43
|
+
print(f"Error: Invalid relative ID format: {raw_id}")
|
|
44
|
+
return None
|
|
45
|
+
else:
|
|
46
|
+
resolved_ids.append(int(raw_id))
|
|
47
|
+
|
|
48
|
+
if relative_indices:
|
|
49
|
+
max_depth = max(relative_indices)
|
|
50
|
+
history_rows = get_history(limit=max_depth)
|
|
51
|
+
|
|
52
|
+
for rel_val in relative_indices:
|
|
53
|
+
list_index = rel_val - 1
|
|
54
|
+
if list_index < len(history_rows):
|
|
55
|
+
real_id = history_rows[list_index][0]
|
|
56
|
+
resolved_ids.append(real_id)
|
|
57
|
+
else:
|
|
58
|
+
print(
|
|
59
|
+
f"Error: Relative ID {rel_val} is out of range (only {len(history_rows)} records available)."
|
|
60
|
+
)
|
|
61
|
+
return None
|
|
62
|
+
|
|
63
|
+
resolved_ids = sorted(list(set(resolved_ids)))
|
|
64
|
+
full_content = not summarize
|
|
65
|
+
context_str = get_interaction_context(resolved_ids, full=full_content)
|
|
66
|
+
if context_str:
|
|
67
|
+
print(f"\n[Loaded context from IDs: {', '.join(map(str, resolved_ids))}]")
|
|
68
|
+
return context_str
|
|
69
|
+
except ValueError:
|
|
70
|
+
print(
|
|
71
|
+
"Error: Invalid format for -c/--continue-chat. Use comma-separated integers or ~N for relative."
|
|
72
|
+
)
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def build_messages(
|
|
77
|
+
args: argparse.Namespace, context_str: str, query_text: str
|
|
78
|
+
) -> List[Dict[str, str]]:
|
|
79
|
+
"""Build the initial message list for the conversation."""
|
|
80
|
+
messages = [
|
|
81
|
+
{
|
|
82
|
+
"role": "system",
|
|
83
|
+
"content": construct_system_prompt(
|
|
84
|
+
args.deep_research, args.deep_dive, args.force_search
|
|
85
|
+
),
|
|
86
|
+
},
|
|
87
|
+
]
|
|
88
|
+
|
|
89
|
+
if context_str:
|
|
90
|
+
messages.append(
|
|
91
|
+
{
|
|
92
|
+
"role": "user",
|
|
93
|
+
"content": f"Context from previous queries:\n{context_str}\n\nMy new query is below.",
|
|
94
|
+
}
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
messages.append({"role": "user", "content": query_text})
|
|
98
|
+
return messages
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def run_chat(args: argparse.Namespace, query_text: str) -> None:
|
|
102
|
+
"""Run the chat conversation flow."""
|
|
103
|
+
# Handle Context
|
|
104
|
+
context_str = ""
|
|
105
|
+
if args.continue_ids:
|
|
106
|
+
context_str = load_context(args.continue_ids, args.summarize)
|
|
107
|
+
if context_str is None:
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
messages = build_messages(args, context_str, query_text)
|
|
111
|
+
|
|
112
|
+
# Initialize Components
|
|
113
|
+
usage_tracker = UsageTracker()
|
|
114
|
+
|
|
115
|
+
if args.deep_dive:
|
|
116
|
+
registry = create_deep_dive_tool_registry()
|
|
117
|
+
else:
|
|
118
|
+
registry = create_default_tool_registry()
|
|
119
|
+
|
|
120
|
+
model_config = MODELS[args.model]
|
|
121
|
+
|
|
122
|
+
engine = ConversationEngine(
|
|
123
|
+
model_config=model_config,
|
|
124
|
+
tool_registry=registry,
|
|
125
|
+
summarize=args.summarize,
|
|
126
|
+
verbose=args.verbose,
|
|
127
|
+
usage_tracker=usage_tracker,
|
|
128
|
+
open_browser=args.open,
|
|
129
|
+
deep_dive=args.deep_dive,
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Run loop
|
|
133
|
+
try:
|
|
134
|
+
final_answer = engine.run(messages)
|
|
135
|
+
|
|
136
|
+
# Save Interaction
|
|
137
|
+
if final_answer:
|
|
138
|
+
print("\n[Saving interaction...]")
|
|
139
|
+
query_summary, answer_summary = generate_summaries(
|
|
140
|
+
query_text, final_answer, usage_tracker=usage_tracker
|
|
141
|
+
)
|
|
142
|
+
save_interaction(
|
|
143
|
+
query_text, final_answer, args.model, query_summary, answer_summary
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
# Send Email if requested
|
|
147
|
+
if final_answer and getattr(args, "mail_recipients", None):
|
|
148
|
+
from asky.email_sender import send_email
|
|
149
|
+
|
|
150
|
+
recipients = [x.strip() for x in args.mail_recipients.split(",")]
|
|
151
|
+
email_subject = args.subject or f"asky Result: {query_text[:50]}"
|
|
152
|
+
send_email(recipients, email_subject, final_answer)
|
|
153
|
+
|
|
154
|
+
except KeyboardInterrupt:
|
|
155
|
+
print("\nAborted by user.")
|
|
156
|
+
except Exception as e:
|
|
157
|
+
print(f"\nAn error occurred: {e}")
|
|
158
|
+
if args.verbose:
|
|
159
|
+
import traceback
|
|
160
|
+
|
|
161
|
+
traceback.print_exc()
|
|
162
|
+
finally:
|
|
163
|
+
# Report usage
|
|
164
|
+
if usage_tracker.usage:
|
|
165
|
+
print("\n=== SESSION TOKEN USAGE ===")
|
|
166
|
+
total_session_tokens = 0
|
|
167
|
+
for m_alias, tokens in usage_tracker.usage.items():
|
|
168
|
+
print(f" {m_alias:<15}: {tokens:,} tokens")
|
|
169
|
+
total_session_tokens += tokens
|
|
170
|
+
print("-" * 30)
|
|
171
|
+
print(f" {'TOTAL':<15}: {total_session_tokens:,} tokens")
|
|
172
|
+
print("===========================\n")
|
asky/cli/history.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""History-related CLI commands for asky."""
|
|
2
|
+
|
|
3
|
+
from rich.console import Console
|
|
4
|
+
from rich.markdown import Markdown
|
|
5
|
+
|
|
6
|
+
from asky.core import is_markdown
|
|
7
|
+
from asky.rendering import render_to_browser
|
|
8
|
+
from asky.storage import (
|
|
9
|
+
get_history,
|
|
10
|
+
get_interaction_context,
|
|
11
|
+
cleanup_db,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def show_history_command(history_arg: int) -> None:
|
|
16
|
+
"""Display recent query history."""
|
|
17
|
+
limit = history_arg if history_arg > 0 else 10
|
|
18
|
+
rows = get_history(limit)
|
|
19
|
+
print(f"\nLast {len(rows)} Queries:")
|
|
20
|
+
print("-" * 60)
|
|
21
|
+
for row in rows:
|
|
22
|
+
rid = row[0]
|
|
23
|
+
query = row[2]
|
|
24
|
+
query_summary = row[3]
|
|
25
|
+
answer_summary = row[4]
|
|
26
|
+
|
|
27
|
+
display_query = query_summary if query_summary else query
|
|
28
|
+
a_sum = answer_summary if answer_summary else ""
|
|
29
|
+
|
|
30
|
+
if len(display_query) > 50:
|
|
31
|
+
display_query = display_query[:47] + "..."
|
|
32
|
+
if len(a_sum) > 50:
|
|
33
|
+
a_sum = a_sum[:47] + "..."
|
|
34
|
+
|
|
35
|
+
print(f"{rid:<4} | {display_query:<50} | {a_sum:<50}")
|
|
36
|
+
print("-" * 60)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def print_answers_command(
|
|
40
|
+
ids_str: str,
|
|
41
|
+
summarize: bool,
|
|
42
|
+
open_browser: bool = False,
|
|
43
|
+
mail_recipients: str = None,
|
|
44
|
+
subject: str = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Print answers for specific history IDs."""
|
|
47
|
+
try:
|
|
48
|
+
ids = [int(x.strip()) for x in ids_str.split(",")]
|
|
49
|
+
except ValueError:
|
|
50
|
+
print("Error: Invalid ID format. Use comma-separated integers.")
|
|
51
|
+
return
|
|
52
|
+
|
|
53
|
+
context = get_interaction_context(ids, full=not summarize)
|
|
54
|
+
if not context:
|
|
55
|
+
print("No records found for the given IDs.")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
print(f"\n[Retrieving answers for IDs: {ids_str}]\n")
|
|
59
|
+
print("-" * 60)
|
|
60
|
+
if is_markdown(context):
|
|
61
|
+
console = Console()
|
|
62
|
+
console.print(Markdown(context))
|
|
63
|
+
else:
|
|
64
|
+
print(context)
|
|
65
|
+
print("-" * 60)
|
|
66
|
+
|
|
67
|
+
if open_browser:
|
|
68
|
+
render_to_browser(context)
|
|
69
|
+
|
|
70
|
+
if mail_recipients:
|
|
71
|
+
from asky.email_sender import send_email
|
|
72
|
+
|
|
73
|
+
recipients = [x.strip() for x in mail_recipients.split(",")]
|
|
74
|
+
# Use provided subject or default
|
|
75
|
+
email_subject = subject or f"asky History: {ids_str}"
|
|
76
|
+
send_email(recipients, email_subject, context)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def handle_cleanup_command(args) -> bool:
|
|
80
|
+
"""Handle cleanup-db flag."""
|
|
81
|
+
if args.cleanup_db or (args.cleanup_db is None and getattr(args, "all", False)):
|
|
82
|
+
if args.all:
|
|
83
|
+
count = cleanup_db(delete_all=True)
|
|
84
|
+
print(f"Deleted all {count} records from history.")
|
|
85
|
+
elif "-" in args.cleanup_db or "," in args.cleanup_db:
|
|
86
|
+
count = cleanup_db(ids=args.cleanup_db)
|
|
87
|
+
print(f"Deleted {count} records from history.")
|
|
88
|
+
else:
|
|
89
|
+
try:
|
|
90
|
+
days = int(args.cleanup_db)
|
|
91
|
+
count = cleanup_db(days=days)
|
|
92
|
+
print(f"Deleted {count} records older than {days} days.")
|
|
93
|
+
except ValueError:
|
|
94
|
+
print(
|
|
95
|
+
"Error: Invalid cleanup format. Use days (int), range (start-end), or list (1,2,3)."
|
|
96
|
+
)
|
|
97
|
+
return True
|
|
98
|
+
return False
|
asky/cli/main.py
ADDED
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"""Command-line interface for asky."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import re
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from rich.console import Console
|
|
8
|
+
|
|
9
|
+
from asky.config import (
|
|
10
|
+
DEFAULT_MODEL,
|
|
11
|
+
MODELS,
|
|
12
|
+
SUMMARIZATION_MODEL,
|
|
13
|
+
SEARCH_PROVIDER,
|
|
14
|
+
DEFAULT_CONTEXT_SIZE,
|
|
15
|
+
MAX_TURNS,
|
|
16
|
+
QUERY_SUMMARY_MAX_CHARS,
|
|
17
|
+
ANSWER_SUMMARY_MAX_CHARS,
|
|
18
|
+
LOG_LEVEL,
|
|
19
|
+
LOG_FILE,
|
|
20
|
+
)
|
|
21
|
+
from asky.banner import get_banner
|
|
22
|
+
from asky.logger import setup_logging
|
|
23
|
+
from asky.storage import init_db, get_db_record_count
|
|
24
|
+
from . import history, prompts, chat, utils
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_args() -> argparse.Namespace:
|
|
28
|
+
"""Parse command-line arguments."""
|
|
29
|
+
parser = argparse.ArgumentParser(
|
|
30
|
+
description="Tool-calling CLI with model selection.",
|
|
31
|
+
formatter_class=argparse.RawTextHelpFormatter,
|
|
32
|
+
)
|
|
33
|
+
parser.add_argument(
|
|
34
|
+
"-m",
|
|
35
|
+
"--model",
|
|
36
|
+
default=DEFAULT_MODEL,
|
|
37
|
+
choices=MODELS.keys(),
|
|
38
|
+
help="Select the model alias",
|
|
39
|
+
)
|
|
40
|
+
parser.add_argument(
|
|
41
|
+
"-d",
|
|
42
|
+
"--deep-research",
|
|
43
|
+
nargs="?",
|
|
44
|
+
type=int,
|
|
45
|
+
const=5,
|
|
46
|
+
default=0,
|
|
47
|
+
help="Enable deep research mode (optional: specify min number of queries, default 5)",
|
|
48
|
+
)
|
|
49
|
+
parser.add_argument(
|
|
50
|
+
"-dd",
|
|
51
|
+
"--deep-dive",
|
|
52
|
+
action="store_true",
|
|
53
|
+
help="Enable deep dive mode (extracts links and encourages reading more pages from same domain)",
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
"-c",
|
|
57
|
+
"--continue-chat",
|
|
58
|
+
dest="continue_ids",
|
|
59
|
+
help="Continue conversation with context from specific history IDs (comma-separated, e.g. '1,2').",
|
|
60
|
+
)
|
|
61
|
+
parser.add_argument(
|
|
62
|
+
"-s",
|
|
63
|
+
"--summarize",
|
|
64
|
+
action="store_true",
|
|
65
|
+
help="Enable summarize mode (summarizes URL content and uses summaries for chat context)",
|
|
66
|
+
)
|
|
67
|
+
parser.add_argument(
|
|
68
|
+
"-fs",
|
|
69
|
+
"--force-search",
|
|
70
|
+
action="store_true",
|
|
71
|
+
help="Force the model to use web search (default: False).\n"
|
|
72
|
+
"Helpful for avoiding hallucinations with small models",
|
|
73
|
+
)
|
|
74
|
+
parser.add_argument(
|
|
75
|
+
"--cleanup-db",
|
|
76
|
+
nargs="?",
|
|
77
|
+
const="interactive",
|
|
78
|
+
help="Delete history records. usage: --cleanup-db [ID|ID-ID|ID,ID] or --cleanup-db --all",
|
|
79
|
+
)
|
|
80
|
+
parser.add_argument(
|
|
81
|
+
"--all",
|
|
82
|
+
action="store_true",
|
|
83
|
+
help="Used with --cleanup-db to delete ALL history.",
|
|
84
|
+
)
|
|
85
|
+
parser.add_argument(
|
|
86
|
+
"-H",
|
|
87
|
+
"--history",
|
|
88
|
+
nargs="?",
|
|
89
|
+
type=int,
|
|
90
|
+
const=10,
|
|
91
|
+
help="Show last N queries and answer summaries (default 10).\n"
|
|
92
|
+
"Use with --print-answer to print the full answer(s).",
|
|
93
|
+
)
|
|
94
|
+
parser.add_argument(
|
|
95
|
+
"-pa",
|
|
96
|
+
"--print-answer",
|
|
97
|
+
dest="print_ids",
|
|
98
|
+
help="Print the answer(s) for specific history IDs (comma-separated).",
|
|
99
|
+
)
|
|
100
|
+
parser.add_argument(
|
|
101
|
+
"-p",
|
|
102
|
+
"--prompts",
|
|
103
|
+
action="store_true",
|
|
104
|
+
help="List all configured user prompts.",
|
|
105
|
+
)
|
|
106
|
+
parser.add_argument(
|
|
107
|
+
"-v",
|
|
108
|
+
"--verbose",
|
|
109
|
+
action="store_true",
|
|
110
|
+
help="Enable verbose output (prints config and LLM inputs).",
|
|
111
|
+
)
|
|
112
|
+
parser.add_argument(
|
|
113
|
+
"-o",
|
|
114
|
+
"--open",
|
|
115
|
+
action="store_true",
|
|
116
|
+
help="Open the final answer in a browser using a markdown template.",
|
|
117
|
+
)
|
|
118
|
+
parser.add_argument(
|
|
119
|
+
"--mail",
|
|
120
|
+
dest="mail_recipients",
|
|
121
|
+
help="Send the final answer via email to comma-separated addresses.",
|
|
122
|
+
)
|
|
123
|
+
parser.add_argument(
|
|
124
|
+
"--subject",
|
|
125
|
+
help="Subject line for the email (used with --mail).",
|
|
126
|
+
)
|
|
127
|
+
parser.add_argument("query", nargs="*", help="The query string")
|
|
128
|
+
return parser.parse_args()
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def show_banner(args) -> None:
|
|
132
|
+
"""Display the application banner."""
|
|
133
|
+
model_alias = args.model
|
|
134
|
+
model_id = MODELS[model_alias]["id"]
|
|
135
|
+
sum_alias = SUMMARIZATION_MODEL
|
|
136
|
+
sum_id = MODELS[sum_alias]["id"]
|
|
137
|
+
|
|
138
|
+
model_ctx = MODELS[model_alias].get("context_size", DEFAULT_CONTEXT_SIZE)
|
|
139
|
+
sum_ctx = MODELS[sum_alias].get("context_size", DEFAULT_CONTEXT_SIZE)
|
|
140
|
+
db_count = get_db_record_count()
|
|
141
|
+
|
|
142
|
+
banner = get_banner(
|
|
143
|
+
model_alias,
|
|
144
|
+
model_id,
|
|
145
|
+
sum_alias,
|
|
146
|
+
sum_id,
|
|
147
|
+
DEFAULT_MODEL,
|
|
148
|
+
SEARCH_PROVIDER,
|
|
149
|
+
model_ctx,
|
|
150
|
+
sum_ctx,
|
|
151
|
+
MAX_TURNS,
|
|
152
|
+
db_count,
|
|
153
|
+
)
|
|
154
|
+
Console().print(banner)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def handle_print_answer_implicit(args) -> bool:
|
|
158
|
+
"""Handle implicit print answer (query is list of ints)."""
|
|
159
|
+
if not args.query:
|
|
160
|
+
return False
|
|
161
|
+
query_str = " ".join(args.query).strip()
|
|
162
|
+
if re.match(r"^(\d+\s*,?\s*)+$", query_str):
|
|
163
|
+
possible_ids = re.split(r"[,\s]+", query_str)
|
|
164
|
+
clean_ids_str = ",".join([x for x in possible_ids if x])
|
|
165
|
+
history.print_answers_command(
|
|
166
|
+
clean_ids_str,
|
|
167
|
+
args.summarize,
|
|
168
|
+
open_browser=args.open,
|
|
169
|
+
mail_recipients=args.mail_recipients,
|
|
170
|
+
subject=args.subject,
|
|
171
|
+
)
|
|
172
|
+
return True
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def main() -> None:
|
|
177
|
+
"""Main entry point."""
|
|
178
|
+
setup_logging(LOG_LEVEL, LOG_FILE)
|
|
179
|
+
args = parse_args()
|
|
180
|
+
init_db()
|
|
181
|
+
|
|
182
|
+
# Commands that don't require banner or query
|
|
183
|
+
if args.history is not None:
|
|
184
|
+
history.show_history_command(args.history)
|
|
185
|
+
return
|
|
186
|
+
if history.handle_cleanup_command(args):
|
|
187
|
+
return
|
|
188
|
+
if args.print_ids:
|
|
189
|
+
history.print_answers_command(
|
|
190
|
+
args.print_ids,
|
|
191
|
+
args.summarize,
|
|
192
|
+
open_browser=args.open,
|
|
193
|
+
mail_recipients=args.mail_recipients,
|
|
194
|
+
subject=args.subject,
|
|
195
|
+
)
|
|
196
|
+
return
|
|
197
|
+
if handle_print_answer_implicit(args):
|
|
198
|
+
return
|
|
199
|
+
if args.prompts:
|
|
200
|
+
prompts.list_prompts_command()
|
|
201
|
+
return
|
|
202
|
+
|
|
203
|
+
# From here on, we need a query
|
|
204
|
+
if not args.query:
|
|
205
|
+
print("Error: Query argument is required.")
|
|
206
|
+
return
|
|
207
|
+
|
|
208
|
+
# Expand query
|
|
209
|
+
query_text = utils.expand_query_text(" ".join(args.query), verbose=args.verbose)
|
|
210
|
+
|
|
211
|
+
# Verbose config
|
|
212
|
+
if args.verbose:
|
|
213
|
+
utils.print_config(
|
|
214
|
+
args,
|
|
215
|
+
MODELS,
|
|
216
|
+
DEFAULT_MODEL,
|
|
217
|
+
MAX_TURNS,
|
|
218
|
+
QUERY_SUMMARY_MAX_CHARS,
|
|
219
|
+
ANSWER_SUMMARY_MAX_CHARS,
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
# Show banner
|
|
223
|
+
show_banner(args)
|
|
224
|
+
|
|
225
|
+
# Run Chat
|
|
226
|
+
chat.run_chat(args, query_text)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
if __name__ == "__main__":
|
|
230
|
+
main()
|
asky/cli/prompts.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Prompt-related CLI commands for asky."""
|
|
2
|
+
|
|
3
|
+
from asky.config import USER_PROMPTS
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def list_prompts_command() -> None:
|
|
7
|
+
"""List all configured user prompts."""
|
|
8
|
+
if not USER_PROMPTS:
|
|
9
|
+
print("\nNo user prompts configured.")
|
|
10
|
+
return
|
|
11
|
+
|
|
12
|
+
print("\nConfigured User Prompts:")
|
|
13
|
+
print("-" * 40)
|
|
14
|
+
for alias, prompt in USER_PROMPTS.items():
|
|
15
|
+
print(
|
|
16
|
+
f"[{alias}]: {prompt[:100]}..."
|
|
17
|
+
if len(prompt) > 100
|
|
18
|
+
else f"[{alias}]: {prompt}"
|
|
19
|
+
)
|
|
20
|
+
print("-" * 40)
|