q-bot 2.0.0.dev1__tar.gz → 2.0.0.dev2__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.
- q_bot-2.0.0.dev2/PKG-INFO +188 -0
- q_bot-2.0.0.dev2/README.md +169 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev2}/pyproject.toml +1 -1
- q_bot-2.0.0.dev2/q/__init__.py +1 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev2}/q/cli/commands.py +22 -60
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev2}/q/cli/main.py +4 -8
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev2}/q/cli/models.py +3 -3
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev2}/q/cli/parser.py +14 -12
- q_bot-2.0.0.dev2/q/cli/session.py +135 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev2}/q/cli/terminal.py +2 -2
- q_bot-2.0.0.dev2/q_bot.egg-info/PKG-INFO +188 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev2}/q_bot.egg-info/requires.txt +1 -1
- q_bot-2.0.0.dev1/PKG-INFO +0 -75
- q_bot-2.0.0.dev1/README.md +0 -56
- q_bot-2.0.0.dev1/q/__init__.py +0 -1
- q_bot-2.0.0.dev1/q/cli/session.py +0 -207
- q_bot-2.0.0.dev1/q_bot.egg-info/PKG-INFO +0 -75
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev2}/q/agents.py +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev2}/q/client.py +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev2}/q/message.py +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev2}/q/providers/__init__.py +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev2}/q/providers/anthropic.py +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev2}/q/providers/openai.py +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev2}/q_bot.egg-info/SOURCES.txt +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev2}/q_bot.egg-info/dependency_links.txt +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev2}/q_bot.egg-info/entry_points.txt +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev2}/q_bot.egg-info/top_level.txt +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev2}/setup.cfg +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: q-bot
|
|
3
|
+
Version: 2.0.0.dev2
|
|
4
|
+
Summary: An LLM agent from the comfort of your command line
|
|
5
|
+
Author-email: Tushar Khan <dev@tusharkhan.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Repository, https://github.com/tk755/q
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
Requires-Dist: anthropic==0.75.0
|
|
11
|
+
Requires-Dist: colorama==0.4.6
|
|
12
|
+
Requires-Dist: distro==1.9.0
|
|
13
|
+
Requires-Dist: openai==2.9.0
|
|
14
|
+
Requires-Dist: psutil==7.2.1
|
|
15
|
+
Requires-Dist: pydantic==2.12.5
|
|
16
|
+
Requires-Dist: pyperclip==1.11.0
|
|
17
|
+
Requires-Dist: python-dotenv==1.2.1
|
|
18
|
+
Requires-Dist: termcolor==3.2.0
|
|
19
|
+
|
|
20
|
+
# Overview
|
|
21
|
+
|
|
22
|
+
`q` is a provider-agnostic command-line agent and LLM framework.
|
|
23
|
+
|
|
24
|
+
> I originally built this as a personal CLI tool before Claude Code existed. I still find it more useful for quick shell interactions and running multi-model experiments.
|
|
25
|
+
|
|
26
|
+
# Installation
|
|
27
|
+
|
|
28
|
+
Install using any pip-compatible package manager (e.g. `pip`, `pipx`, `uv`, etc.):
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pipx install q-bot
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Requires Python 3.12+.
|
|
35
|
+
|
|
36
|
+
# CLI Usage
|
|
37
|
+
|
|
38
|
+
`q` uses a simple paradigm where each character from a-z is mapped to a single flag representing a command or option. This enables concise combinations of flags to achieve complex behavior.
|
|
39
|
+
|
|
40
|
+
## Flag Reference
|
|
41
|
+
|
|
42
|
+
| Flag | Name | Arg | Description | Type |
|
|
43
|
+
| ---- | ------------ | ------- | ------------------------------ | ------: |
|
|
44
|
+
| `-a` | agent | | *[reserved for future use]* | Command |
|
|
45
|
+
| `-b` | batch | | *[reserved for future use]* | |
|
|
46
|
+
| `-c` | code | str | generate code | Command |
|
|
47
|
+
| `-d` | directory | - / str | add a directory to context | Option |
|
|
48
|
+
| `-e` | explain | - / str | explain code or text | Command |
|
|
49
|
+
| `-f` | file | str | read input from file | Option |
|
|
50
|
+
| `-g` | | | | |
|
|
51
|
+
| `-h` | help | - / str | help message / help agent | Command |
|
|
52
|
+
| `-i` | image | str | generate/edit an image | Command |
|
|
53
|
+
| `-j` | json | - | output as JSON | Option |
|
|
54
|
+
| `-k` | api key | str | override API key | Option |
|
|
55
|
+
| `-l` | | | | |
|
|
56
|
+
| `-m` | model | str | override model/provider | Option |
|
|
57
|
+
| `-n` | new session | - | clear the session history | Option |
|
|
58
|
+
| `-o` | output | str | output file | Option |
|
|
59
|
+
| `-p` | | | | |
|
|
60
|
+
| `-q` | | | | |
|
|
61
|
+
| `-r` | rag | - / str | *[reserved for future use]* | Command |
|
|
62
|
+
| `-s` | shell | - / str | generate a shell command | Command |
|
|
63
|
+
| `-t` | text | str | generate text | Command |
|
|
64
|
+
| `-u` | user command | str | *[reserved for future use]* | Command |
|
|
65
|
+
| `-v` | verbose | - | debug logging | Option |
|
|
66
|
+
| `-w` | web search | str | search the web | Command |
|
|
67
|
+
| `-x` | execute | - | execute a shell command | Option |
|
|
68
|
+
| `-y` | | | | |
|
|
69
|
+
| `-z` | undo | - / int | undo exchanges (default 1) | Option |
|
|
70
|
+
|
|
71
|
+
## Sessions
|
|
72
|
+
|
|
73
|
+
Each terminal or script that runs `q` maintains an isolated **session** that persists conversation history across calls. Use `-n` to clear the session history for a new conversation or one-shot prompt. Sessions are automatically deleted when the parent shell process exits.
|
|
74
|
+
|
|
75
|
+
# Library Usage
|
|
76
|
+
|
|
77
|
+
The `q` library is built on two principles:
|
|
78
|
+
|
|
79
|
+
**Clients are single-capability.** Each client does one thing (e.g. text generation, image generation, web search, etc.) and has a static return type. No mode switching or tool selection logic is necessary.
|
|
80
|
+
|
|
81
|
+
**Agents are provider- and capability-agnostic.** Every agent accepts any client and inherits its return type, regardless of what the underlying client does or which provider it calls.
|
|
82
|
+
|
|
83
|
+
## Clients
|
|
84
|
+
|
|
85
|
+
A **client** wraps a provider's API for one capability.
|
|
86
|
+
|
|
87
|
+
Clients extend `Client[T]` and are instantiated with an API key, model name, and optionally provider- and model-specific argument overrides. All clients expose the same `generate` method which returns a value of type `T`:
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
Client[T](api_key: str, model: str, **model_args)
|
|
91
|
+
Client[T].generate(messages: list[Message]) -> T
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
A number of built-in clients with sensible defaults are provided for the following providers and capabilities:
|
|
95
|
+
|
|
96
|
+
| Client | T | Description | `openai` | `anthropic` |
|
|
97
|
+
| ------------- | ------- | ---------------------------- | :------: | :---------: |
|
|
98
|
+
| `TextClient` | `str` | text generation | ✓ | ✓ |
|
|
99
|
+
| `WebClient` | `str` | web-grounded text generation | ✓ | ✗ |
|
|
100
|
+
| `ImageClient` | `bytes` | image generation | ✓ | ✗ |
|
|
101
|
+
|
|
102
|
+
### Dynamic Loading
|
|
103
|
+
|
|
104
|
+
Client classes are typically imported from their provider module:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from q.providers.openai import ImageClient
|
|
108
|
+
|
|
109
|
+
client = ImageClient(api_key, model, **model_args)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
They can also be dynamically loaded at runtime by specifying a provider and capability using the `load_client_class` utility:
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from q.providers import load_client_class
|
|
116
|
+
|
|
117
|
+
client_class = load_client_class('openai', 'ImageClient')
|
|
118
|
+
client = client_class(api_key, model, **model_args)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
## Agents
|
|
122
|
+
|
|
123
|
+
An **agent** manages conversation state and delegates generation to a client.
|
|
124
|
+
|
|
125
|
+
`ChatAgent[T]` maintains a message history and prepends an optional system prompt:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
ChatAgent[T](client: Client[T], system: str | None = None)
|
|
129
|
+
ChatAgent[T].prompt(text: str) -> T
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
`BatchAgent[T]` processes multiple inputs concurrently using a shared system prompt, with no conversation history:
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
BatchAgent[T](client: Client[T], system: str | None = None)
|
|
136
|
+
BatchAgent[T].batch_prompt(text_list: list[str], n_threads: int = 8) -> list[T]
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
<!-- ## Examples
|
|
140
|
+
|
|
141
|
+
This design enables full LLM functionality with minimal code.
|
|
142
|
+
|
|
143
|
+
**Example 1:** Generate an image via OpenAI
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from q.providers.openai import ImageClient
|
|
147
|
+
from q.agents import ChatAgent
|
|
148
|
+
|
|
149
|
+
client = ImageClient(api_key, "gpt-image-2", quality="high")
|
|
150
|
+
agent = ChatAgent(client)
|
|
151
|
+
image_bytes = await agent.prompt("a cat in space")
|
|
152
|
+
Path("cat.png").write_bytes(image_bytes)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Example 2:** Generate batch text via Anthropic
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
from q.providers.anthropic import TextClient
|
|
159
|
+
from q.agents import BatchAgent
|
|
160
|
+
|
|
161
|
+
client = TextClient(api_key, "claude-opus-4-8")
|
|
162
|
+
agent = BatchAgent(client, system="Identify the language of the text.")
|
|
163
|
+
inputs = ["How are you?", "¿Cómo estás?", "Comment ça va?"]
|
|
164
|
+
langs = await agent.batch_prompt(inputs)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
**Example 3:** Multi-model orchestration
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from q.providers import load_client_class
|
|
171
|
+
from q.agents import ChatAgent
|
|
172
|
+
|
|
173
|
+
client1 = load_client_class('openai', 'TextClient')(oai_key, "gpt-5-5")
|
|
174
|
+
client2 = load_client_class('anthropic', 'TextClient')(anthropic_key, "claude-opus-4-8")
|
|
175
|
+
|
|
176
|
+
system = "You are an AI speaking with another AI. Engage in a discussion about the future of AI."
|
|
177
|
+
|
|
178
|
+
agent1 = ChatAgent(client1, system=system)
|
|
179
|
+
agent2 = ChatAgent(client2, system=system)
|
|
180
|
+
|
|
181
|
+
text = "What are your thoughts on the future of AI?"
|
|
182
|
+
|
|
183
|
+
for _ in range(5):
|
|
184
|
+
text = await agent1.prompt(text)
|
|
185
|
+
print("agent1:", text)
|
|
186
|
+
text = await agent2.prompt(text)
|
|
187
|
+
print("agent2:", text)
|
|
188
|
+
``` -->
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Overview
|
|
2
|
+
|
|
3
|
+
`q` is a provider-agnostic command-line agent and LLM framework.
|
|
4
|
+
|
|
5
|
+
> I originally built this as a personal CLI tool before Claude Code existed. I still find it more useful for quick shell interactions and running multi-model experiments.
|
|
6
|
+
|
|
7
|
+
# Installation
|
|
8
|
+
|
|
9
|
+
Install using any pip-compatible package manager (e.g. `pip`, `pipx`, `uv`, etc.):
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
pipx install q-bot
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Requires Python 3.12+.
|
|
16
|
+
|
|
17
|
+
# CLI Usage
|
|
18
|
+
|
|
19
|
+
`q` uses a simple paradigm where each character from a-z is mapped to a single flag representing a command or option. This enables concise combinations of flags to achieve complex behavior.
|
|
20
|
+
|
|
21
|
+
## Flag Reference
|
|
22
|
+
|
|
23
|
+
| Flag | Name | Arg | Description | Type |
|
|
24
|
+
| ---- | ------------ | ------- | ------------------------------ | ------: |
|
|
25
|
+
| `-a` | agent | | *[reserved for future use]* | Command |
|
|
26
|
+
| `-b` | batch | | *[reserved for future use]* | |
|
|
27
|
+
| `-c` | code | str | generate code | Command |
|
|
28
|
+
| `-d` | directory | - / str | add a directory to context | Option |
|
|
29
|
+
| `-e` | explain | - / str | explain code or text | Command |
|
|
30
|
+
| `-f` | file | str | read input from file | Option |
|
|
31
|
+
| `-g` | | | | |
|
|
32
|
+
| `-h` | help | - / str | help message / help agent | Command |
|
|
33
|
+
| `-i` | image | str | generate/edit an image | Command |
|
|
34
|
+
| `-j` | json | - | output as JSON | Option |
|
|
35
|
+
| `-k` | api key | str | override API key | Option |
|
|
36
|
+
| `-l` | | | | |
|
|
37
|
+
| `-m` | model | str | override model/provider | Option |
|
|
38
|
+
| `-n` | new session | - | clear the session history | Option |
|
|
39
|
+
| `-o` | output | str | output file | Option |
|
|
40
|
+
| `-p` | | | | |
|
|
41
|
+
| `-q` | | | | |
|
|
42
|
+
| `-r` | rag | - / str | *[reserved for future use]* | Command |
|
|
43
|
+
| `-s` | shell | - / str | generate a shell command | Command |
|
|
44
|
+
| `-t` | text | str | generate text | Command |
|
|
45
|
+
| `-u` | user command | str | *[reserved for future use]* | Command |
|
|
46
|
+
| `-v` | verbose | - | debug logging | Option |
|
|
47
|
+
| `-w` | web search | str | search the web | Command |
|
|
48
|
+
| `-x` | execute | - | execute a shell command | Option |
|
|
49
|
+
| `-y` | | | | |
|
|
50
|
+
| `-z` | undo | - / int | undo exchanges (default 1) | Option |
|
|
51
|
+
|
|
52
|
+
## Sessions
|
|
53
|
+
|
|
54
|
+
Each terminal or script that runs `q` maintains an isolated **session** that persists conversation history across calls. Use `-n` to clear the session history for a new conversation or one-shot prompt. Sessions are automatically deleted when the parent shell process exits.
|
|
55
|
+
|
|
56
|
+
# Library Usage
|
|
57
|
+
|
|
58
|
+
The `q` library is built on two principles:
|
|
59
|
+
|
|
60
|
+
**Clients are single-capability.** Each client does one thing (e.g. text generation, image generation, web search, etc.) and has a static return type. No mode switching or tool selection logic is necessary.
|
|
61
|
+
|
|
62
|
+
**Agents are provider- and capability-agnostic.** Every agent accepts any client and inherits its return type, regardless of what the underlying client does or which provider it calls.
|
|
63
|
+
|
|
64
|
+
## Clients
|
|
65
|
+
|
|
66
|
+
A **client** wraps a provider's API for one capability.
|
|
67
|
+
|
|
68
|
+
Clients extend `Client[T]` and are instantiated with an API key, model name, and optionally provider- and model-specific argument overrides. All clients expose the same `generate` method which returns a value of type `T`:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
Client[T](api_key: str, model: str, **model_args)
|
|
72
|
+
Client[T].generate(messages: list[Message]) -> T
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
A number of built-in clients with sensible defaults are provided for the following providers and capabilities:
|
|
76
|
+
|
|
77
|
+
| Client | T | Description | `openai` | `anthropic` |
|
|
78
|
+
| ------------- | ------- | ---------------------------- | :------: | :---------: |
|
|
79
|
+
| `TextClient` | `str` | text generation | ✓ | ✓ |
|
|
80
|
+
| `WebClient` | `str` | web-grounded text generation | ✓ | ✗ |
|
|
81
|
+
| `ImageClient` | `bytes` | image generation | ✓ | ✗ |
|
|
82
|
+
|
|
83
|
+
### Dynamic Loading
|
|
84
|
+
|
|
85
|
+
Client classes are typically imported from their provider module:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
from q.providers.openai import ImageClient
|
|
89
|
+
|
|
90
|
+
client = ImageClient(api_key, model, **model_args)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
They can also be dynamically loaded at runtime by specifying a provider and capability using the `load_client_class` utility:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from q.providers import load_client_class
|
|
97
|
+
|
|
98
|
+
client_class = load_client_class('openai', 'ImageClient')
|
|
99
|
+
client = client_class(api_key, model, **model_args)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
## Agents
|
|
103
|
+
|
|
104
|
+
An **agent** manages conversation state and delegates generation to a client.
|
|
105
|
+
|
|
106
|
+
`ChatAgent[T]` maintains a message history and prepends an optional system prompt:
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
ChatAgent[T](client: Client[T], system: str | None = None)
|
|
110
|
+
ChatAgent[T].prompt(text: str) -> T
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
`BatchAgent[T]` processes multiple inputs concurrently using a shared system prompt, with no conversation history:
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
BatchAgent[T](client: Client[T], system: str | None = None)
|
|
117
|
+
BatchAgent[T].batch_prompt(text_list: list[str], n_threads: int = 8) -> list[T]
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
<!-- ## Examples
|
|
121
|
+
|
|
122
|
+
This design enables full LLM functionality with minimal code.
|
|
123
|
+
|
|
124
|
+
**Example 1:** Generate an image via OpenAI
|
|
125
|
+
|
|
126
|
+
```python
|
|
127
|
+
from q.providers.openai import ImageClient
|
|
128
|
+
from q.agents import ChatAgent
|
|
129
|
+
|
|
130
|
+
client = ImageClient(api_key, "gpt-image-2", quality="high")
|
|
131
|
+
agent = ChatAgent(client)
|
|
132
|
+
image_bytes = await agent.prompt("a cat in space")
|
|
133
|
+
Path("cat.png").write_bytes(image_bytes)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Example 2:** Generate batch text via Anthropic
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from q.providers.anthropic import TextClient
|
|
140
|
+
from q.agents import BatchAgent
|
|
141
|
+
|
|
142
|
+
client = TextClient(api_key, "claude-opus-4-8")
|
|
143
|
+
agent = BatchAgent(client, system="Identify the language of the text.")
|
|
144
|
+
inputs = ["How are you?", "¿Cómo estás?", "Comment ça va?"]
|
|
145
|
+
langs = await agent.batch_prompt(inputs)
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Example 3:** Multi-model orchestration
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
from q.providers import load_client_class
|
|
152
|
+
from q.agents import ChatAgent
|
|
153
|
+
|
|
154
|
+
client1 = load_client_class('openai', 'TextClient')(oai_key, "gpt-5-5")
|
|
155
|
+
client2 = load_client_class('anthropic', 'TextClient')(anthropic_key, "claude-opus-4-8")
|
|
156
|
+
|
|
157
|
+
system = "You are an AI speaking with another AI. Engage in a discussion about the future of AI."
|
|
158
|
+
|
|
159
|
+
agent1 = ChatAgent(client1, system=system)
|
|
160
|
+
agent2 = ChatAgent(client2, system=system)
|
|
161
|
+
|
|
162
|
+
text = "What are your thoughts on the future of AI?"
|
|
163
|
+
|
|
164
|
+
for _ in range(5):
|
|
165
|
+
text = await agent1.prompt(text)
|
|
166
|
+
print("agent1:", text)
|
|
167
|
+
text = await agent2.prompt(text)
|
|
168
|
+
print("agent2:", text)
|
|
169
|
+
``` -->
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.0.0.dev2"
|
|
@@ -4,7 +4,6 @@ import asyncio
|
|
|
4
4
|
import contextlib
|
|
5
5
|
import os
|
|
6
6
|
import platform
|
|
7
|
-
import shutil
|
|
8
7
|
import string
|
|
9
8
|
import subprocess
|
|
10
9
|
import sys
|
|
@@ -13,7 +12,6 @@ from enum import Enum
|
|
|
13
12
|
from pathlib import Path
|
|
14
13
|
|
|
15
14
|
import distro
|
|
16
|
-
import humanize
|
|
17
15
|
import pyperclip
|
|
18
16
|
from termcolor import colored
|
|
19
17
|
|
|
@@ -21,10 +19,9 @@ from q import __version__
|
|
|
21
19
|
from q.providers import load_client_class
|
|
22
20
|
|
|
23
21
|
from ..agents import ChatAgent
|
|
24
|
-
from ..message import Role
|
|
25
22
|
from .models import Tier, resolve_model_arg
|
|
26
|
-
from .session import
|
|
27
|
-
from .terminal import
|
|
23
|
+
from .session import StateManager
|
|
24
|
+
from .terminal import InputError, format_response, qprint
|
|
28
25
|
|
|
29
26
|
# region Registry
|
|
30
27
|
|
|
@@ -33,7 +30,7 @@ OPTIONS: list[type[Flag]] = []
|
|
|
33
30
|
|
|
34
31
|
|
|
35
32
|
def get_default_command() -> type[Command]:
|
|
36
|
-
char =
|
|
33
|
+
char = StateManager.load_command_char()
|
|
37
34
|
if char:
|
|
38
35
|
for cmd in COMMANDS:
|
|
39
36
|
if cmd.char == char:
|
|
@@ -93,22 +90,19 @@ class Command(Flag):
|
|
|
93
90
|
class AgentCommand(Command):
|
|
94
91
|
"""Base class for commands that prompt an agent."""
|
|
95
92
|
|
|
96
|
-
client_str: str = "TextClient"
|
|
97
93
|
tier: Tier
|
|
94
|
+
client_str: str = "TextClient"
|
|
98
95
|
system: str | None = None
|
|
99
96
|
clip: bool = False
|
|
100
97
|
|
|
101
98
|
async def execute(self) -> None:
|
|
102
|
-
if "n" in self.args:
|
|
103
|
-
SessionManager.new_session()
|
|
104
|
-
|
|
105
99
|
# resolve provider, model, and model args
|
|
106
|
-
default_provider =
|
|
100
|
+
default_provider = StateManager.load_default_provider()
|
|
107
101
|
provider, model, model_args = resolve_model_arg(self.args.get("m"), self.tier, default_provider)
|
|
108
102
|
|
|
109
103
|
# create client dynamically
|
|
110
104
|
client_class = load_client_class(provider, self.client_str)
|
|
111
|
-
api_key =
|
|
105
|
+
api_key = self.args.get("k") or StateManager.load_api_key(provider)
|
|
112
106
|
client = client_class(api_key, model, **model_args)
|
|
113
107
|
|
|
114
108
|
if "v" in self.args:
|
|
@@ -120,17 +114,17 @@ class AgentCommand(Command):
|
|
|
120
114
|
qprint(f"{k}: ", color="green", file=sys.stderr, end="")
|
|
121
115
|
qprint(f"{v}", file=sys.stderr)
|
|
122
116
|
|
|
123
|
-
# resolve system prompt (command's system overrides saved system)
|
|
124
|
-
system = self.system if self.system is not None else SessionManager.load_system()
|
|
125
|
-
|
|
126
117
|
# create agent
|
|
127
|
-
|
|
118
|
+
messages = [] if "n" in self.args else StateManager.load_messages()
|
|
119
|
+
agent = ChatAgent(client, self.system, messages)
|
|
128
120
|
if "z" in self.args:
|
|
129
121
|
agent.drop_exchanges(self.args["z"])
|
|
130
122
|
|
|
131
|
-
# prompt agent
|
|
123
|
+
# prompt agent
|
|
132
124
|
response = await agent.prompt(self.args[self.char])
|
|
133
|
-
|
|
125
|
+
|
|
126
|
+
# save session
|
|
127
|
+
StateManager.save_session(self.char, agent.messages)
|
|
134
128
|
|
|
135
129
|
if "v" in self.args:
|
|
136
130
|
qprint("\nMESSAGES:", color="cyan", file=sys.stderr)
|
|
@@ -179,6 +173,7 @@ class ExplainCommand(AgentCommand):
|
|
|
179
173
|
char = "e"
|
|
180
174
|
desc = "explain"
|
|
181
175
|
value_type = ValueType.TEXT
|
|
176
|
+
required = True
|
|
182
177
|
tier = Tier.HIGH
|
|
183
178
|
system = "You are a programming assistant. Given a shell command, code snippet, or technical concept, provide a concise and technical explanation. Assume the reader is an experienced developer. Avoid restating the code or command. Avoid explaining obvious syntax. Avoid breaking the answer into bullet points unless necessary. The response should be a single short paragraph optimized for clarity."
|
|
184
179
|
|
|
@@ -193,7 +188,7 @@ class CodeCommand(AgentCommand):
|
|
|
193
188
|
|
|
194
189
|
@property
|
|
195
190
|
def system(self) -> str:
|
|
196
|
-
return f"You are a coding assistant. Given a natural language description, generate a code snippet that accomplishes the requested task. The code should be correct, efficient, concise, and idiomatic. Respond with only the code snippet, without explanations, additional text, or formatting. Assume the programming language is {
|
|
191
|
+
return f"You are a coding assistant. Given a natural language description, generate a code snippet that accomplishes the requested task. The code should be correct, efficient, concise, and idiomatic. Respond with only the code snippet, without explanations, additional text, or formatting. Assume the programming language is {StateManager.load_code_lang()} unless otherwise specified."
|
|
197
192
|
|
|
198
193
|
|
|
199
194
|
class ShellCommand(AgentCommand):
|
|
@@ -227,7 +222,7 @@ class ShellCommand(AgentCommand):
|
|
|
227
222
|
cmd = os.environ.get("Q_CMD", None)
|
|
228
223
|
exit_code = os.environ.get("Q_EXIT", None)
|
|
229
224
|
if cmd is None or exit_code is None:
|
|
230
|
-
raise
|
|
225
|
+
raise InputError(
|
|
231
226
|
"q -s without a prompt requires shell integration. Add to ~/.bashrc:\n"
|
|
232
227
|
' q() { Q_EXIT=$? Q_CMD=$(fc -ln -1) command q "$@"; }'
|
|
233
228
|
)
|
|
@@ -337,46 +332,6 @@ class HelpCommand(AgentCommand):
|
|
|
337
332
|
return "\n".join(lines)
|
|
338
333
|
|
|
339
334
|
|
|
340
|
-
class LoadCommand(Command):
|
|
341
|
-
char = "l"
|
|
342
|
-
desc = "load session"
|
|
343
|
-
value_type = ValueType.INT
|
|
344
|
-
|
|
345
|
-
async def execute(self) -> None:
|
|
346
|
-
session_id = self.args.get(self.char)
|
|
347
|
-
if session_id is None:
|
|
348
|
-
self._print_session_list()
|
|
349
|
-
elif not SessionManager.switch_session(session_id):
|
|
350
|
-
raise UserError(f"invalid session: {session_id}")
|
|
351
|
-
else:
|
|
352
|
-
qprint(f"Loaded session {session_id}", color="yellow", file=sys.stderr)
|
|
353
|
-
|
|
354
|
-
def _print_session_list(self) -> None:
|
|
355
|
-
sessions = SessionManager.list_sessions()
|
|
356
|
-
if not sessions:
|
|
357
|
-
qprint("No sessions found.")
|
|
358
|
-
return
|
|
359
|
-
|
|
360
|
-
current_id = SessionManager.load_session_id()
|
|
361
|
-
term_width = shutil.get_terminal_size().columns
|
|
362
|
-
|
|
363
|
-
for s in sessions:
|
|
364
|
-
age = humanize.naturaltime(s.updated) if s.updated else "unknown"
|
|
365
|
-
prefix_len = len(f" {s.id}. ")
|
|
366
|
-
suffix_len = len(f" ({age})")
|
|
367
|
-
max_len = max(20, term_width - prefix_len - suffix_len - 5)
|
|
368
|
-
|
|
369
|
-
preview = "(empty)"
|
|
370
|
-
for msg in reversed(s.messages):
|
|
371
|
-
if msg.role == Role.USER:
|
|
372
|
-
preview = msg.content[:max_len] + "..." if len(msg.content) > max_len else msg.content
|
|
373
|
-
break
|
|
374
|
-
|
|
375
|
-
line = f" {s.id}. {preview} ({age})"
|
|
376
|
-
color = None if s.id == current_id else "dark_grey"
|
|
377
|
-
qprint(line, color=color)
|
|
378
|
-
|
|
379
|
-
|
|
380
335
|
# region Options
|
|
381
336
|
|
|
382
337
|
|
|
@@ -398,6 +353,13 @@ class JsonOption(Flag):
|
|
|
398
353
|
desc = "json"
|
|
399
354
|
|
|
400
355
|
|
|
356
|
+
class KeyOption(Flag):
|
|
357
|
+
char = "k"
|
|
358
|
+
desc = "api key"
|
|
359
|
+
value_type = ValueType.STR
|
|
360
|
+
required = True
|
|
361
|
+
|
|
362
|
+
|
|
401
363
|
class ModelOption(Flag):
|
|
402
364
|
char = "m"
|
|
403
365
|
desc = "model"
|
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
-
import os
|
|
3
2
|
import sys
|
|
4
|
-
from pathlib import Path
|
|
5
3
|
|
|
6
4
|
from .parser import parse
|
|
7
|
-
from .
|
|
5
|
+
from .session import StateManager
|
|
6
|
+
from .terminal import InputError, qprint
|
|
8
7
|
|
|
9
8
|
|
|
10
9
|
def main():
|
|
11
|
-
# suppress stderr when piped
|
|
12
|
-
if not is_terminal():
|
|
13
|
-
sys.stderr = Path(os.devnull).open("w") # noqa: SIM115
|
|
14
|
-
|
|
15
10
|
try:
|
|
16
11
|
command = parse(sys.argv[1:])
|
|
12
|
+
StateManager.reap_sessions()
|
|
17
13
|
asyncio.run(command.execute())
|
|
18
|
-
except (
|
|
14
|
+
except (InputError, ImportError) as e:
|
|
19
15
|
qprint(str(e), color="red", file=sys.stderr)
|
|
20
16
|
sys.exit(1)
|
|
21
17
|
except KeyboardInterrupt:
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import contextlib
|
|
2
2
|
from enum import Enum
|
|
3
3
|
|
|
4
|
-
from .terminal import
|
|
4
|
+
from .terminal import InputError
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class Tier(Enum):
|
|
@@ -57,7 +57,7 @@ MODELS = {
|
|
|
57
57
|
|
|
58
58
|
def _lookup(provider: str, tier: Tier) -> tuple[str, str, dict]:
|
|
59
59
|
if provider not in MODELS:
|
|
60
|
-
raise
|
|
60
|
+
raise InputError(f"unknown provider: {provider}")
|
|
61
61
|
|
|
62
62
|
config = MODELS[provider][tier]
|
|
63
63
|
model = config["model"]
|
|
@@ -87,4 +87,4 @@ def resolve_model_arg(arg: str | None, default_tier: Tier, default_provider: str
|
|
|
87
87
|
if arg in MODELS:
|
|
88
88
|
return _lookup(arg, default_tier)
|
|
89
89
|
|
|
90
|
-
raise
|
|
90
|
+
raise InputError(f"invalid model: {arg}")
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import re
|
|
2
2
|
|
|
3
|
-
from .commands import COMMANDS, OPTIONS, ArgMap, Command, Flag, ValueType, get_default_command
|
|
4
|
-
from .terminal import
|
|
3
|
+
from .commands import COMMANDS, OPTIONS, ArgMap, Command, Flag, HelpCommand, ValueType, get_default_command
|
|
4
|
+
from .terminal import InputError
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
def _resolve_pending(pending_flags: list[type[Flag]], pending_tokens: list[str]) -> ArgMap:
|
|
@@ -32,13 +32,13 @@ def _resolve_pending(pending_flags: list[type[Flag]], pending_tokens: list[str])
|
|
|
32
32
|
if len(required) == 1:
|
|
33
33
|
consumer = required[0]
|
|
34
34
|
elif len(required) > 1:
|
|
35
|
-
raise
|
|
35
|
+
raise InputError(f"ambiguous target for '{pending_tokens[0]}': " + ", ".join(f"-{f.char}" for f in required))
|
|
36
36
|
elif len(optional) == 1:
|
|
37
37
|
consumer = optional[0]
|
|
38
38
|
elif len(optional) > 1:
|
|
39
|
-
raise
|
|
39
|
+
raise InputError(f"ambiguous target for '{pending_tokens[0]}': " + ", ".join(f"-{f.char}" for f in optional))
|
|
40
40
|
else:
|
|
41
|
-
raise
|
|
41
|
+
raise InputError(
|
|
42
42
|
", ".join(f"-{f.char}" for f in pending_flags)
|
|
43
43
|
+ f" received invalid argument{('s' if len(pending_tokens) > 1 else '')}: "
|
|
44
44
|
+ ", ".join(f"'{t}'" for t in pending_tokens)
|
|
@@ -49,7 +49,7 @@ def _resolve_pending(pending_flags: list[type[Flag]], pending_tokens: list[str])
|
|
|
49
49
|
value = " ".join(pending_tokens)
|
|
50
50
|
else: # STR or INT
|
|
51
51
|
if len(pending_tokens) > 1:
|
|
52
|
-
raise
|
|
52
|
+
raise InputError(
|
|
53
53
|
f"-{consumer.char} expects one token but got: " + ", ".join(f"'{t}'" for t in pending_tokens)
|
|
54
54
|
)
|
|
55
55
|
value = pending_tokens[0]
|
|
@@ -57,7 +57,7 @@ def _resolve_pending(pending_flags: list[type[Flag]], pending_tokens: list[str])
|
|
|
57
57
|
try:
|
|
58
58
|
value = int(value)
|
|
59
59
|
except ValueError:
|
|
60
|
-
raise
|
|
60
|
+
raise InputError(f"-{consumer.char} expects an integer but got: '{pending_tokens[0]}'") from None
|
|
61
61
|
|
|
62
62
|
# resolve flag values
|
|
63
63
|
args: ArgMap = {}
|
|
@@ -67,7 +67,7 @@ def _resolve_pending(pending_flags: list[type[Flag]], pending_tokens: list[str])
|
|
|
67
67
|
if f == consumer:
|
|
68
68
|
args[f.char] = value
|
|
69
69
|
elif f.required:
|
|
70
|
-
raise
|
|
70
|
+
raise InputError(f"-{f.char} requires a value")
|
|
71
71
|
else:
|
|
72
72
|
args[f.char] = f.default
|
|
73
73
|
return args
|
|
@@ -100,7 +100,7 @@ def parse(argv: list[str]) -> Command:
|
|
|
100
100
|
|
|
101
101
|
duplicates = set(resolved.keys()) & set(args.keys())
|
|
102
102
|
if duplicates:
|
|
103
|
-
raise
|
|
103
|
+
raise InputError(
|
|
104
104
|
f"duplicate flag{'' if len(duplicates) == 1 else 's'}: " + ", ".join(f"-{k}" for k in duplicates)
|
|
105
105
|
)
|
|
106
106
|
|
|
@@ -114,7 +114,7 @@ def parse(argv: list[str]) -> Command:
|
|
|
114
114
|
if is_flag:
|
|
115
115
|
for c in token[1:]:
|
|
116
116
|
if c not in flag_lookup:
|
|
117
|
-
raise
|
|
117
|
+
raise InputError(f"unknown flag: -{c}")
|
|
118
118
|
pending_flags.append(flag_lookup[c])
|
|
119
119
|
pos += 1
|
|
120
120
|
continue
|
|
@@ -129,8 +129,10 @@ def parse(argv: list[str]) -> Command:
|
|
|
129
129
|
valid_commands = {f.char for f in COMMANDS}
|
|
130
130
|
commands = [c for c in args if c in valid_commands]
|
|
131
131
|
if len(commands) > 1:
|
|
132
|
-
raise
|
|
132
|
+
raise InputError("multiple commands: " + ", ".join(f"-{c}" for c in commands))
|
|
133
133
|
if not commands:
|
|
134
|
-
|
|
134
|
+
if not args:
|
|
135
|
+
return HelpCommand(args)
|
|
136
|
+
raise InputError("no command specified")
|
|
135
137
|
|
|
136
138
|
return flag_lookup[commands[0]](args)
|