q-bot 1.4.0__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.
@@ -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,37 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "q-bot"
7
+ dynamic = ["version"]
8
+ description = "An LLM agent from the comfort of your command line"
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "anthropic==0.75.0",
12
+ "colorama==0.4.6",
13
+ "distro==1.9.0",
14
+ "openai==2.9.0",
15
+ "psutil==7.2.1",
16
+ "pydantic==2.12.5",
17
+ "pyperclip==1.11.0",
18
+ "python-dotenv==1.2.1",
19
+ "termcolor==3.2.0",
20
+ ]
21
+ authors = [
22
+ {name = "Tushar Khan", email = "dev@tusharkhan.com"}
23
+ ]
24
+ readme = "README.md"
25
+ license = "MIT"
26
+
27
+ [project.scripts]
28
+ q = "q.cli.main:main"
29
+
30
+ [project.urls]
31
+ Repository = "https://github.com/tk755/q"
32
+
33
+ [tool.setuptools.packages.find]
34
+ include = ["q*"]
35
+
36
+ [tool.setuptools.dynamic]
37
+ version = {attr = "q.__version__"}
@@ -0,0 +1 @@
1
+ __version__ = "2.0.0.dev2"
@@ -0,0 +1,63 @@
1
+ import asyncio
2
+
3
+ from .client import Client
4
+ from .message import Message, Role
5
+
6
+
7
+ class ChatAgent[T]:
8
+ """Conversational agent with persistent message history."""
9
+
10
+ def __init__(self, client: Client[T], system: str | None = None, messages: list[Message] | None = None):
11
+ self.client = client
12
+ self.system = system
13
+ self.messages: list[Message] = messages.copy() if messages else []
14
+
15
+ async def prompt(self, text: str) -> T:
16
+ """Generate response and update conversation history."""
17
+ self.messages.append(Message(role=Role.USER, content=text))
18
+
19
+ messages = self.messages
20
+ if self.system:
21
+ messages = [Message(role=Role.SYSTEM, content=self.system), *self.messages]
22
+ response = await self.client.generate(messages)
23
+
24
+ if isinstance(response, str):
25
+ self.messages.append(Message(role=Role.ASSISTANT, content=response))
26
+
27
+ return response
28
+
29
+ def drop_exchanges(self, n: int = 1) -> None:
30
+ """Drop the last N conversation exchanges (user message + responses)."""
31
+ if n <= 0:
32
+ return
33
+
34
+ user_messages_found = 0
35
+ for i in range(len(self.messages) - 1, -1, -1):
36
+ if self.messages[i].role == Role.USER:
37
+ user_messages_found += 1
38
+ if user_messages_found == n:
39
+ self.messages = self.messages[:i]
40
+ return
41
+
42
+
43
+ class BatchAgent[T]:
44
+ """Batch agent for applying a single prompt to multiple inputs concurrently."""
45
+
46
+ def __init__(self, client: Client[T], system: str | None = None):
47
+ self.client = client
48
+ self.system = system
49
+
50
+ async def batch_prompt(self, text_list: list[str], n_threads: int = 8) -> list[T]:
51
+ """Process multiple inputs concurrently and return the outputs in order."""
52
+ semaphore = asyncio.Semaphore(n_threads)
53
+
54
+ async def process(text: str) -> T:
55
+ async with semaphore:
56
+ messages: list[Message] = []
57
+ if self.system:
58
+ messages.append(Message(role=Role.SYSTEM, content=self.system))
59
+ messages.append(Message(role=Role.USER, content=text))
60
+ return await self.client.generate(messages)
61
+
62
+ tasks = [process(text) for text in text_list]
63
+ return await asyncio.gather(*tasks)