q-bot 2.0.0.dev1__tar.gz → 2.0.0.dev3__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.dev3/PKG-INFO +188 -0
- q_bot-2.0.0.dev3/README.md +169 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/pyproject.toml +2 -2
- q_bot-2.0.0.dev3/q/__init__.py +1 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/cli/commands.py +29 -60
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/cli/main.py +4 -8
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/cli/models.py +3 -3
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/cli/parser.py +14 -12
- q_bot-2.0.0.dev3/q/cli/session.py +135 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/cli/terminal.py +2 -2
- q_bot-2.0.0.dev3/q_bot.egg-info/PKG-INFO +188 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/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.dev3}/q/agents.py +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/client.py +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/message.py +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/providers/__init__.py +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/providers/anthropic.py +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q/providers/openai.py +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q_bot.egg-info/SOURCES.txt +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q_bot.egg-info/dependency_links.txt +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q_bot.egg-info/entry_points.txt +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/q_bot.egg-info/top_level.txt +0 -0
- {q_bot-2.0.0.dev1 → q_bot-2.0.0.dev3}/setup.cfg +0 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: q-bot
|
|
3
|
+
Version: 2.0.0.dev3
|
|
4
|
+
Summary: A provider-agnostic command-line agent and LLM framework.
|
|
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]* | Command |
|
|
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 / 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` | code lang | str | override code generation language | Option |
|
|
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 | 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]* | Command |
|
|
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 / 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` | code lang | str | override code generation language | Option |
|
|
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 | 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
|
+
``` -->
|
|
@@ -5,14 +5,14 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "q-bot"
|
|
7
7
|
dynamic = ["version"]
|
|
8
|
-
description = "
|
|
8
|
+
description = "A provider-agnostic command-line agent and LLM framework."
|
|
9
9
|
requires-python = ">=3.12"
|
|
10
10
|
dependencies = [
|
|
11
11
|
"anthropic==0.75.0",
|
|
12
12
|
"colorama==0.4.6",
|
|
13
13
|
"distro==1.9.0",
|
|
14
|
-
"humanize==4.14.0",
|
|
15
14
|
"openai==2.9.0",
|
|
15
|
+
"psutil==7.2.1",
|
|
16
16
|
"pydantic==2.12.5",
|
|
17
17
|
"pyperclip==1.11.0",
|
|
18
18
|
"python-dotenv==1.2.1",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "2.0.0.dev3"
|
|
@@ -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,8 @@ class CodeCommand(AgentCommand):
|
|
|
193
188
|
|
|
194
189
|
@property
|
|
195
190
|
def system(self) -> str:
|
|
196
|
-
|
|
191
|
+
code_lang = self.args.get("l") or StateManager.load_code_lang()
|
|
192
|
+
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. Use the {code_lang} programming language."
|
|
197
193
|
|
|
198
194
|
|
|
199
195
|
class ShellCommand(AgentCommand):
|
|
@@ -227,7 +223,7 @@ class ShellCommand(AgentCommand):
|
|
|
227
223
|
cmd = os.environ.get("Q_CMD", None)
|
|
228
224
|
exit_code = os.environ.get("Q_EXIT", None)
|
|
229
225
|
if cmd is None or exit_code is None:
|
|
230
|
-
raise
|
|
226
|
+
raise InputError(
|
|
231
227
|
"q -s without a prompt requires shell integration. Add to ~/.bashrc:\n"
|
|
232
228
|
' q() { Q_EXIT=$? Q_CMD=$(fc -ln -1) command q "$@"; }'
|
|
233
229
|
)
|
|
@@ -337,46 +333,6 @@ class HelpCommand(AgentCommand):
|
|
|
337
333
|
return "\n".join(lines)
|
|
338
334
|
|
|
339
335
|
|
|
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
336
|
# region Options
|
|
381
337
|
|
|
382
338
|
|
|
@@ -398,6 +354,19 @@ class JsonOption(Flag):
|
|
|
398
354
|
desc = "json"
|
|
399
355
|
|
|
400
356
|
|
|
357
|
+
class KeyOption(Flag):
|
|
358
|
+
char = "k"
|
|
359
|
+
desc = "api key"
|
|
360
|
+
value_type = ValueType.STR
|
|
361
|
+
required = True
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
class CodeLangOption(Flag):
|
|
365
|
+
char = "l"
|
|
366
|
+
desc = "code lang"
|
|
367
|
+
value_type = ValueType.STR
|
|
368
|
+
|
|
369
|
+
|
|
401
370
|
class ModelOption(Flag):
|
|
402
371
|
char = "m"
|
|
403
372
|
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}")
|