ollama-coding-agent 0.5.2__tar.gz → 0.5.4__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.
Files changed (24) hide show
  1. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/PKG-INFO +149 -149
  2. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/coding_agent/agent.py +4 -1
  3. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/coding_agent/cli.py +28 -4
  4. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/coding_agent/config.py +1 -1
  5. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/coding_agent/ollama_client.py +108 -73
  6. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/coding_agent/ui.py +9 -1
  7. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/ollama_coding_agent.egg-info/PKG-INFO +149 -149
  8. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/pyproject.toml +35 -35
  9. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/setup.cfg +4 -4
  10. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/LICENSE +0 -0
  11. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/PYPI_README.md +0 -0
  12. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/README.md +0 -0
  13. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/coding_agent/__init__.py +0 -0
  14. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/coding_agent/__main__.py +0 -0
  15. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/coding_agent/intent.py +0 -0
  16. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/coding_agent/mcp_client.py +0 -0
  17. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/coding_agent/mcp_server.py +0 -0
  18. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/coding_agent/session_store.py +0 -0
  19. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/coding_agent/tools.py +0 -0
  20. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/ollama_coding_agent.egg-info/SOURCES.txt +0 -0
  21. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/ollama_coding_agent.egg-info/dependency_links.txt +0 -0
  22. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/ollama_coding_agent.egg-info/entry_points.txt +0 -0
  23. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/ollama_coding_agent.egg-info/requires.txt +0 -0
  24. {ollama_coding_agent-0.5.2 → ollama_coding_agent-0.5.4}/ollama_coding_agent.egg-info/top_level.txt +0 -0
@@ -1,149 +1,149 @@
1
- Metadata-Version: 2.4
2
- Name: ollama-coding-agent
3
- Version: 0.5.2
4
- Summary: AI coding agent driving Qwen Coder (or any Ollama-compatible model) through a scoped toolset via MCP
5
- License-Expression: MIT
6
- Project-URL: Homepage, https://github.com/HarryChen1995/coding_agent
7
- Project-URL: Repository, https://github.com/HarryChen1995/coding_agent
8
- Project-URL: Issues, https://github.com/HarryChen1995/coding_agent/issues
9
- Classifier: Environment :: Console
10
- Classifier: Intended Audience :: Developers
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Topic :: Software Development :: Build Tools
13
- Requires-Python: >=3.10
14
- Description-Content-Type: text/markdown
15
- License-File: LICENSE
16
- Requires-Dist: httpx>=0.28
17
- Requires-Dist: rich>=15.0
18
- Requires-Dist: typer>=0.27
19
- Requires-Dist: mcp>=1.27
20
- Requires-Dist: prompt_toolkit>=3.0
21
- Dynamic: license-file
22
-
23
- # Ollama Coding Agent
24
-
25
- An AI coding agent that plans, edits, and tests code by driving Qwen Coder
26
- (or any Ollama-compatible model) through a scoped set of file and shell
27
- tools, with human approval on every write, edit, or shell command.
28
-
29
- ## Install
30
-
31
- ```bash
32
- pip install ollama-coding-agent
33
- ollama pull qwen3-coder:30b
34
- ```
35
-
36
- ## Usage
37
-
38
- ```bash
39
- coding-agent "Add type hints to utils.py, then run the test suite" \
40
- --project-root ./myrepo
41
- ```
42
-
43
- Equivalent: `python -m coding_agent "..." --project-root ./myrepo`.
44
-
45
- Omit the task string to enter an interactive session instead:
46
-
47
- ```bash
48
- coding-agent --project-root ./myrepo
49
- ```
50
-
51
- Run `coding-agent --help` for the full option list.
52
-
53
- ## Features
54
-
55
- - **Structured intent parsing** — the raw task is classified (bug fix,
56
- feature, refactor, risk level, target files) before any action is taken,
57
- and high-risk tasks force human approval even under `--auto-approve`.
58
- - **Session persistence** — every message is saved to SQLite as the run
59
- happens. Resume a previous run by id or a name you gave it
60
- (`--resume`), browse saved sessions (`--list-sessions`), or delete one
61
- (`--delete-session`).
62
- - **Interactive mode** — drop into a REPL that keeps the model connection
63
- and tool session alive across turns.
64
- - **Human-in-the-loop approval** — every write, edit, or shell command
65
- shows a diff or command preview before you confirm, unless explicitly
66
- marked safe or run with `--auto-approve`.
67
- - **Retry and recovery** — transient model failures retry with backoff;
68
- malformed tool-call output is caught and reported back to the model
69
- instead of crashing the run.
70
- - **Codebase exploration tools** — regex content search with glob
71
- filtering, pattern-based file discovery, directory listing, and git diff,
72
- all skipping noise directories (`.git`, `node_modules`, build output).
73
- - **Extensible via custom MCP servers** — point at any MCP server, local
74
- (stdio) or remote (SSE / Streamable HTTP), and its tools merge into the
75
- model's toolset automatically, no code changes required. Register one
76
- permanently (`--add-mcp-server`, available on every future run) or add
77
- one per run (`--mcp-server`/`--mcp-config`).
78
-
79
- ## Architecture
80
-
81
- Tools are served over the Model Context Protocol (MCP), not called
82
- in-process — the agent is an MCP *client* that talks to a tool server over
83
- stdio:
84
-
85
- ```
86
- +-----------------------------+
87
- | CLI / REPL |
88
- +-----------------------------+
89
- |
90
- v
91
- +-----------------------------+
92
- | Agent loop |
93
- | parse intent, call model, |
94
- | approve, execute, persist |
95
- +-----------------------------+
96
- |
97
- v
98
- +-----------------------------+
99
- | MCP client |
100
- | built-in + custom servers |
101
- | merged into one tool list |
102
- +-----------------------------+
103
- |
104
- stdio / SSE / streamable-http
105
- v
106
- +-----------------------------+
107
- | MCP server(s) |
108
- +-----------------------------+
109
- |
110
- v
111
- +-----------------------------+
112
- | Tools |
113
- | read / write / edit / |
114
- | search / shell |
115
- +-----------------------------+
116
- ```
117
-
118
- Because tools are exposed over MCP, any MCP-compatible client — Claude
119
- Desktop, another agent framework, a different model entirely — can reach
120
- the exact same toolset, approval-preview logic, and path scoping. The
121
- reverse also holds: any additional MCP server — local (stdio) or remote
122
- (SSE / Streamable HTTP) — can be plugged into this agent, and its tools
123
- merge into the same list the model already sees —
124
- ```bash
125
- coding-agent --add-mcp-server "weather=python -m weather_mcp_server" # local, stdio
126
- coding-agent --add-mcp-server "weather=https://example.com/mcp/sse" # remote, SSE
127
- coding-agent "what's the forecast?" # picked up automatically, every run from here on
128
- ```
129
- A value after `name=` starting with `http://`/`https://` is treated as a
130
- remote server (SSE by default, append `,streamable_http` for that transport
131
- instead); anything else is a local command spawned over stdio — it doesn't
132
- need to be `-m`-invokable, a standalone script's absolute path works too
133
- (e.g. `"myserver=python C:/absolute/path/to/mcp_server.py"`).
134
-
135
- ## Configuration
136
-
137
- Point at any Ollama-compatible host with `--ollama-host` or the
138
- `OLLAMA_HOST` env var. If it sits behind an authenticated proxy, set
139
- `OLLAMA_API_KEY` as an environment variable rather than a CLI flag so the
140
- key doesn't end up in shell history.
141
-
142
- ## Links
143
-
144
- Source, full documentation, and issue tracker:
145
- https://github.com/HarryChen1995/coding_agent
146
-
147
- ## License
148
-
149
- MIT
1
+ Metadata-Version: 2.4
2
+ Name: ollama-coding-agent
3
+ Version: 0.5.4
4
+ Summary: AI coding agent driving Qwen Coder (or any Ollama-compatible model) through a scoped toolset via MCP
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/HarryChen1995/coding_agent
7
+ Project-URL: Repository, https://github.com/HarryChen1995/coding_agent
8
+ Project-URL: Issues, https://github.com/HarryChen1995/coding_agent/issues
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development :: Build Tools
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: httpx>=0.28
17
+ Requires-Dist: rich>=15.0
18
+ Requires-Dist: typer>=0.27
19
+ Requires-Dist: mcp>=1.27
20
+ Requires-Dist: prompt_toolkit>=3.0
21
+ Dynamic: license-file
22
+
23
+ # Ollama Coding Agent
24
+
25
+ An AI coding agent that plans, edits, and tests code by driving Qwen Coder
26
+ (or any Ollama-compatible model) through a scoped set of file and shell
27
+ tools, with human approval on every write, edit, or shell command.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install ollama-coding-agent
33
+ ollama pull qwen3-coder:30b
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```bash
39
+ coding-agent "Add type hints to utils.py, then run the test suite" \
40
+ --project-root ./myrepo
41
+ ```
42
+
43
+ Equivalent: `python -m coding_agent "..." --project-root ./myrepo`.
44
+
45
+ Omit the task string to enter an interactive session instead:
46
+
47
+ ```bash
48
+ coding-agent --project-root ./myrepo
49
+ ```
50
+
51
+ Run `coding-agent --help` for the full option list.
52
+
53
+ ## Features
54
+
55
+ - **Structured intent parsing** — the raw task is classified (bug fix,
56
+ feature, refactor, risk level, target files) before any action is taken,
57
+ and high-risk tasks force human approval even under `--auto-approve`.
58
+ - **Session persistence** — every message is saved to SQLite as the run
59
+ happens. Resume a previous run by id or a name you gave it
60
+ (`--resume`), browse saved sessions (`--list-sessions`), or delete one
61
+ (`--delete-session`).
62
+ - **Interactive mode** — drop into a REPL that keeps the model connection
63
+ and tool session alive across turns.
64
+ - **Human-in-the-loop approval** — every write, edit, or shell command
65
+ shows a diff or command preview before you confirm, unless explicitly
66
+ marked safe or run with `--auto-approve`.
67
+ - **Retry and recovery** — transient model failures retry with backoff;
68
+ malformed tool-call output is caught and reported back to the model
69
+ instead of crashing the run.
70
+ - **Codebase exploration tools** — regex content search with glob
71
+ filtering, pattern-based file discovery, directory listing, and git diff,
72
+ all skipping noise directories (`.git`, `node_modules`, build output).
73
+ - **Extensible via custom MCP servers** — point at any MCP server, local
74
+ (stdio) or remote (SSE / Streamable HTTP), and its tools merge into the
75
+ model's toolset automatically, no code changes required. Register one
76
+ permanently (`--add-mcp-server`, available on every future run) or add
77
+ one per run (`--mcp-server`/`--mcp-config`).
78
+
79
+ ## Architecture
80
+
81
+ Tools are served over the Model Context Protocol (MCP), not called
82
+ in-process — the agent is an MCP *client* that talks to a tool server over
83
+ stdio:
84
+
85
+ ```
86
+ +-----------------------------+
87
+ | CLI / REPL |
88
+ +-----------------------------+
89
+ |
90
+ v
91
+ +-----------------------------+
92
+ | Agent loop |
93
+ | parse intent, call model, |
94
+ | approve, execute, persist |
95
+ +-----------------------------+
96
+ |
97
+ v
98
+ +-----------------------------+
99
+ | MCP client |
100
+ | built-in + custom servers |
101
+ | merged into one tool list |
102
+ +-----------------------------+
103
+ |
104
+ stdio / SSE / streamable-http
105
+ v
106
+ +-----------------------------+
107
+ | MCP server(s) |
108
+ +-----------------------------+
109
+ |
110
+ v
111
+ +-----------------------------+
112
+ | Tools |
113
+ | read / write / edit / |
114
+ | search / shell |
115
+ +-----------------------------+
116
+ ```
117
+
118
+ Because tools are exposed over MCP, any MCP-compatible client — Claude
119
+ Desktop, another agent framework, a different model entirely — can reach
120
+ the exact same toolset, approval-preview logic, and path scoping. The
121
+ reverse also holds: any additional MCP server — local (stdio) or remote
122
+ (SSE / Streamable HTTP) — can be plugged into this agent, and its tools
123
+ merge into the same list the model already sees —
124
+ ```bash
125
+ coding-agent --add-mcp-server "weather=python -m weather_mcp_server" # local, stdio
126
+ coding-agent --add-mcp-server "weather=https://example.com/mcp/sse" # remote, SSE
127
+ coding-agent "what's the forecast?" # picked up automatically, every run from here on
128
+ ```
129
+ A value after `name=` starting with `http://`/`https://` is treated as a
130
+ remote server (SSE by default, append `,streamable_http` for that transport
131
+ instead); anything else is a local command spawned over stdio — it doesn't
132
+ need to be `-m`-invokable, a standalone script's absolute path works too
133
+ (e.g. `"myserver=python C:/absolute/path/to/mcp_server.py"`).
134
+
135
+ ## Configuration
136
+
137
+ Point at any Ollama-compatible host with `--ollama-host` or the
138
+ `OLLAMA_HOST` env var. If it sits behind an authenticated proxy, set
139
+ `OLLAMA_API_KEY` as an environment variable rather than a CLI flag so the
140
+ key doesn't end up in shell history.
141
+
142
+ ## Links
143
+
144
+ Source, full documentation, and issue tracker:
145
+ https://github.com/HarryChen1995/coding_agent
146
+
147
+ ## License
148
+
149
+ MIT
@@ -114,7 +114,10 @@ def _recover_text_tool_calls(content: str, tool_names: set) -> list:
114
114
  calls.append({
115
115
  "id": f"fallback_{len(calls)}",
116
116
  "type": "function",
117
- "function": {"name": name, "arguments": args},
117
+ "function": {
118
+ "name": name,
119
+ "arguments": json.dumps(args, ensure_ascii=False),
120
+ },
118
121
  })
119
122
  return calls
120
123
 
@@ -32,6 +32,7 @@ Examples:
32
32
 
33
33
  import asyncio
34
34
  import os
35
+ import signal
35
36
  from contextlib import nullcontext
36
37
  from typing import List, Optional
37
38
 
@@ -65,7 +66,7 @@ def main(
65
66
  "(defaults to $OLLAMA_API_KEY — prefer the env var over this flag "
66
67
  "so the key doesn't end up in your shell history).",
67
68
  ),
68
- max_steps: int = typer.Option(25, "--max-steps", help="Hard cap on agent loop iterations"),
69
+ max_steps: int = typer.Option(100, "--max-steps", help="Hard cap on agent loop iterations"),
69
70
  auto_approve: bool = typer.Option(
70
71
  False, "--auto-approve",
71
72
  help="Skip human approval for write/edit/shell tools. Only use in an "
@@ -243,7 +244,8 @@ async def _interactive(cfg: AgentConfig, resume: Optional[str], session_name: Op
243
244
  # literal garbage like "?[32m" on the screen.
244
245
  stdout_cm = patch_stdout(raw=True)
245
246
  except ImportError:
246
- typer.echo(f"Interactive mode (model: {cfg.model}). Type a task, /sessions to list, /exit to quit.\n")
247
+ typer.echo(f"Interactive mode (model: {cfg.model}). Type a task, /sessions to list, /exit to quit. "
248
+ "Ctrl+C interrupts the current turn without leaving the session.\n")
247
249
  prompt_session = None
248
250
  stdout_cm = nullcontext()
249
251
 
@@ -278,12 +280,34 @@ async def _interactive(cfg: AgentConfig, resume: Optional[str], session_name: Op
278
280
  typer.echo(f"No session found with id or name {target!r}.", err=True)
279
281
  continue
280
282
 
283
+ # Run the turn as a Task so Ctrl+C can cancel just this turn
284
+ # (via the SIGINT handler below) instead of killing the whole
285
+ # REPL — a raw KeyboardInterrupt raised inside asyncio's own
286
+ # blocking wait can otherwise escape uncaught past this loop
287
+ # entirely. task.cancel() injects CancelledError at the
288
+ # coroutine's next await point (model call, tool call, etc.),
289
+ # unwinding just that turn; the MCP client and session history
290
+ # already written to disk are untouched, so the REPL keeps going.
291
+ run_task = asyncio.ensure_future(
292
+ agent.run(task, resume_session_id=session_id, client=client,
293
+ session_name=session_name, show_banner=False)
294
+ )
295
+ previous_sigint = signal.signal(signal.SIGINT, lambda *_: run_task.cancel())
281
296
  try:
282
- result = await agent.run(task, resume_session_id=session_id, client=client,
283
- session_name=session_name, show_banner=False)
297
+ result = await run_task
298
+ except asyncio.CancelledError:
299
+ session_id = agent.session_id or session_id
300
+ try:
301
+ from . import ui
302
+ ui.interrupted()
303
+ except ImportError:
304
+ typer.echo("\n[Interrupted — back to prompt. You can keep chatting in this session.]")
305
+ continue
284
306
  except ValueError as e:
285
307
  typer.echo(f"Error: {e}", err=True)
286
308
  continue
309
+ finally:
310
+ signal.signal(signal.SIGINT, previous_sigint)
287
311
 
288
312
  session_id = agent.session_id
289
313
 
@@ -23,7 +23,7 @@ class AgentConfig:
23
23
  )
24
24
 
25
25
  auto_approve: bool = False # True = never prompt (use in CI with care)
26
- max_steps: int = 25 # hard cap on agent loop iterations
26
+ max_steps: int = 100 # hard cap on agent loop iterations
27
27
 
28
28
  # Parse the freeform task into structured intent (task_type, target_files,
29
29
  # constraints, risk_level) before the agent starts acting.
@@ -1,73 +1,108 @@
1
- """Talks to Ollama directly over HTTP (/api/chat) — no `ollama` python
2
- package required, just httpx. Mirrors the message shape the rest of the
3
- agent already expects: chat() returns the `message` dict with
4
- role/content/tool_calls, same as the ollama library did.
5
- """
6
-
7
- import os
8
-
9
- import httpx
10
-
11
- DEFAULT_BASE_URL = os.environ.get("OLLAMA_HOST", "http://localhost:11434")
12
- DEFAULT_API_KEY = os.environ.get("OLLAMA_API_KEY", "") # set via env, never hardcode
13
-
14
-
15
- class OllamaError(Exception):
16
- pass
17
-
18
-
19
- async def chat(
20
- model: str,
21
- messages: list,
22
- tools: list = None,
23
- format: str = None,
24
- base_url: str = None,
25
- api_key: str = None,
26
- timeout: float = 120.0,
27
- ) -> dict:
28
- """POST /api/v1/chat/completions (OpenAI-compatible) with stream=false
29
- and return the `message` dict.
30
-
31
- This hits our Open WebUI gateway in front of Ollama, not Ollama's native
32
- /api/chat — the gateway only exposes the OpenAI-style route. Response
33
- shape is `choices[0].message`, but that dict already has the
34
- role/content/tool_calls keys the rest of the agent expects, same as
35
- Ollama's native `message` field would.
36
-
37
- If an API key is set (via `api_key`, falling back to $OLLAMA_API_KEY),
38
- it's sent as `Authorization: Bearer <key>`.
39
-
40
- Raises OllamaError on a non-2xx response, a connection failure, or an
41
- unexpected response shape — callers (agent.py, intent.py) already retry
42
- on this.
43
- """
44
- url = f"{(base_url or DEFAULT_BASE_URL).rstrip('/')}/v1/chat/completions"
45
- payload = {"model": model, "messages": messages, "stream": False}
46
- if tools:
47
- payload["tools"] = tools
48
- if format == "json":
49
- payload["response_format"] = {"type": "json_object"}
50
-
51
- key = api_key or DEFAULT_API_KEY
52
- headers = {"Authorization": f"Bearer {key}"} if key else {}
53
-
54
- try:
55
- async with httpx.AsyncClient(timeout=timeout) as client:
56
- resp = await client.post(url, json=payload, headers=headers)
57
- resp.raise_for_status()
58
- data = resp.json()
59
- except httpx.HTTPStatusError as e:
60
- body = e.response.text[:300]
61
- raise OllamaError(f"Ollama API returned {e.response.status_code} for {model}: {body}") from e
62
- except httpx.RequestError as e:
63
- raise OllamaError(
64
- f"Could not reach Ollama at {url} ({e}). Is `ollama serve` running?"
65
- ) from e
66
-
67
- choices = data.get("choices")
68
- if not choices:
69
- raise OllamaError(f"Unexpected response shape from Ollama (no 'choices' key): {data}")
70
- message = choices[0].get("message")
71
- if message is None:
72
- raise OllamaError(f"Unexpected response shape from Ollama (no 'message' key): {data}")
73
- return message
1
+ """Talks to Ollama directly over HTTP (/api/chat) — no `ollama` python
2
+ package required, just httpx. Mirrors the message shape the rest of the
3
+ agent already expects: chat() returns the `message` dict with
4
+ role/content/tool_calls, same as the ollama library did.
5
+ """
6
+
7
+ import json
8
+ import os
9
+
10
+ import httpx
11
+
12
+ DEFAULT_BASE_URL = os.environ.get("OLLAMA_HOST", "http://localhost:11434")
13
+ DEFAULT_API_KEY = os.environ.get("OLLAMA_API_KEY", "") # set via env, never hardcode
14
+
15
+
16
+ def _normalize_tool_call(call: dict) -> dict:
17
+ if not isinstance(call, dict) or call.get("type") != "function":
18
+ return call
19
+
20
+ function = call.get("function")
21
+ if not isinstance(function, dict):
22
+ return call
23
+
24
+ args = function.get("arguments")
25
+ if args is None or isinstance(args, str):
26
+ return call
27
+
28
+ normalized = function.copy()
29
+ normalized["arguments"] = json.dumps(args, ensure_ascii=False)
30
+ return {**call, "function": normalized}
31
+
32
+
33
+ def _normalize_messages(messages: list) -> list:
34
+ normalized = []
35
+ for msg in messages:
36
+ if not isinstance(msg, dict):
37
+ normalized.append(msg)
38
+ continue
39
+ tool_calls = msg.get("tool_calls")
40
+ if isinstance(tool_calls, list):
41
+ normalized.append({
42
+ **msg,
43
+ "tool_calls": [_normalize_tool_call(call) for call in tool_calls],
44
+ })
45
+ else:
46
+ normalized.append(msg)
47
+ return normalized
48
+
49
+
50
+ class OllamaError(Exception):
51
+ pass
52
+
53
+
54
+ async def chat(
55
+ model: str,
56
+ messages: list,
57
+ tools: list = None,
58
+ format: str = None,
59
+ base_url: str = None,
60
+ api_key: str = None,
61
+ timeout: float = 120.0,
62
+ ) -> dict:
63
+ """POST /api/v1/chat/completions (OpenAI-compatible) with stream=false
64
+ and return the `message` dict.
65
+
66
+ This hits our Open WebUI gateway in front of Ollama, not Ollama's native
67
+ /api/chat the gateway only exposes the OpenAI-style route. Response
68
+ shape is `choices[0].message`, but that dict already has the
69
+ role/content/tool_calls keys the rest of the agent expects, same as
70
+ Ollama's native `message` field would.
71
+
72
+ If an API key is set (via `api_key`, falling back to $OLLAMA_API_KEY),
73
+ it's sent as `Authorization: Bearer <key>`.
74
+
75
+ Raises OllamaError on a non-2xx response, a connection failure, or an
76
+ unexpected response shape — callers (agent.py, intent.py) already retry
77
+ on this.
78
+ """
79
+ url = f"{(base_url or DEFAULT_BASE_URL).rstrip('/')}/v1/chat/completions"
80
+ payload = {"model": model, "messages": _normalize_messages(messages), "stream": False}
81
+ if tools:
82
+ payload["tools"] = tools
83
+ if format == "json":
84
+ payload["response_format"] = {"type": "json_object"}
85
+
86
+ key = api_key or DEFAULT_API_KEY
87
+ headers = {"Authorization": f"Bearer {key}"} if key else {}
88
+
89
+ try:
90
+ async with httpx.AsyncClient(timeout=timeout) as client:
91
+ resp = await client.post(url, json=payload, headers=headers)
92
+ resp.raise_for_status()
93
+ data = resp.json()
94
+ except httpx.HTTPStatusError as e:
95
+ body = e.response.text[:300]
96
+ raise OllamaError(f"Ollama API returned {e.response.status_code} for {model}: {body}") from e
97
+ except httpx.RequestError as e:
98
+ raise OllamaError(
99
+ f"Could not reach Ollama at {url} ({e}). Is `ollama serve` running?"
100
+ ) from e
101
+
102
+ choices = data.get("choices")
103
+ if not choices:
104
+ raise OllamaError(f"Unexpected response shape from Ollama (no 'choices' key): {data}")
105
+ message = choices[0].get("message")
106
+ if message is None:
107
+ raise OllamaError(f"Unexpected response shape from Ollama (no 'message' key): {data}")
108
+ return message
@@ -44,7 +44,8 @@ def interactive_banner(model: str, resumed: str = None):
44
44
  console.print(f"[dim]model:[/dim] {model}")
45
45
  if resumed:
46
46
  console.print(f"[dim]resuming session:[/dim] {resumed}")
47
- console.print("[dim]Type a task, /sessions to list saved sessions, /exit to quit.[/dim]\n")
47
+ console.print("[dim]Type a task, /sessions to list saved sessions, /exit to quit. "
48
+ "Ctrl+C interrupts the current turn without leaving the session.[/dim]\n")
48
49
 
49
50
 
50
51
  async def prompt_task_async(session) -> str:
@@ -226,6 +227,13 @@ def sessions_table(sessions: list):
226
227
  console.print(table)
227
228
 
228
229
 
230
+ def interrupted():
231
+ console.print(
232
+ "\n[yellow]⏹ Interrupted — back at the prompt. Progress up to the last "
233
+ "completed step was saved; keep chatting or ask the agent to continue.[/yellow]"
234
+ )
235
+
236
+
229
237
  def warning(text: str):
230
238
  console.print(f"[yellow]⚠ {text}[/yellow]")
231
239
 
@@ -1,149 +1,149 @@
1
- Metadata-Version: 2.4
2
- Name: ollama-coding-agent
3
- Version: 0.5.2
4
- Summary: AI coding agent driving Qwen Coder (or any Ollama-compatible model) through a scoped toolset via MCP
5
- License-Expression: MIT
6
- Project-URL: Homepage, https://github.com/HarryChen1995/coding_agent
7
- Project-URL: Repository, https://github.com/HarryChen1995/coding_agent
8
- Project-URL: Issues, https://github.com/HarryChen1995/coding_agent/issues
9
- Classifier: Environment :: Console
10
- Classifier: Intended Audience :: Developers
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Topic :: Software Development :: Build Tools
13
- Requires-Python: >=3.10
14
- Description-Content-Type: text/markdown
15
- License-File: LICENSE
16
- Requires-Dist: httpx>=0.28
17
- Requires-Dist: rich>=15.0
18
- Requires-Dist: typer>=0.27
19
- Requires-Dist: mcp>=1.27
20
- Requires-Dist: prompt_toolkit>=3.0
21
- Dynamic: license-file
22
-
23
- # Ollama Coding Agent
24
-
25
- An AI coding agent that plans, edits, and tests code by driving Qwen Coder
26
- (or any Ollama-compatible model) through a scoped set of file and shell
27
- tools, with human approval on every write, edit, or shell command.
28
-
29
- ## Install
30
-
31
- ```bash
32
- pip install ollama-coding-agent
33
- ollama pull qwen3-coder:30b
34
- ```
35
-
36
- ## Usage
37
-
38
- ```bash
39
- coding-agent "Add type hints to utils.py, then run the test suite" \
40
- --project-root ./myrepo
41
- ```
42
-
43
- Equivalent: `python -m coding_agent "..." --project-root ./myrepo`.
44
-
45
- Omit the task string to enter an interactive session instead:
46
-
47
- ```bash
48
- coding-agent --project-root ./myrepo
49
- ```
50
-
51
- Run `coding-agent --help` for the full option list.
52
-
53
- ## Features
54
-
55
- - **Structured intent parsing** — the raw task is classified (bug fix,
56
- feature, refactor, risk level, target files) before any action is taken,
57
- and high-risk tasks force human approval even under `--auto-approve`.
58
- - **Session persistence** — every message is saved to SQLite as the run
59
- happens. Resume a previous run by id or a name you gave it
60
- (`--resume`), browse saved sessions (`--list-sessions`), or delete one
61
- (`--delete-session`).
62
- - **Interactive mode** — drop into a REPL that keeps the model connection
63
- and tool session alive across turns.
64
- - **Human-in-the-loop approval** — every write, edit, or shell command
65
- shows a diff or command preview before you confirm, unless explicitly
66
- marked safe or run with `--auto-approve`.
67
- - **Retry and recovery** — transient model failures retry with backoff;
68
- malformed tool-call output is caught and reported back to the model
69
- instead of crashing the run.
70
- - **Codebase exploration tools** — regex content search with glob
71
- filtering, pattern-based file discovery, directory listing, and git diff,
72
- all skipping noise directories (`.git`, `node_modules`, build output).
73
- - **Extensible via custom MCP servers** — point at any MCP server, local
74
- (stdio) or remote (SSE / Streamable HTTP), and its tools merge into the
75
- model's toolset automatically, no code changes required. Register one
76
- permanently (`--add-mcp-server`, available on every future run) or add
77
- one per run (`--mcp-server`/`--mcp-config`).
78
-
79
- ## Architecture
80
-
81
- Tools are served over the Model Context Protocol (MCP), not called
82
- in-process — the agent is an MCP *client* that talks to a tool server over
83
- stdio:
84
-
85
- ```
86
- +-----------------------------+
87
- | CLI / REPL |
88
- +-----------------------------+
89
- |
90
- v
91
- +-----------------------------+
92
- | Agent loop |
93
- | parse intent, call model, |
94
- | approve, execute, persist |
95
- +-----------------------------+
96
- |
97
- v
98
- +-----------------------------+
99
- | MCP client |
100
- | built-in + custom servers |
101
- | merged into one tool list |
102
- +-----------------------------+
103
- |
104
- stdio / SSE / streamable-http
105
- v
106
- +-----------------------------+
107
- | MCP server(s) |
108
- +-----------------------------+
109
- |
110
- v
111
- +-----------------------------+
112
- | Tools |
113
- | read / write / edit / |
114
- | search / shell |
115
- +-----------------------------+
116
- ```
117
-
118
- Because tools are exposed over MCP, any MCP-compatible client — Claude
119
- Desktop, another agent framework, a different model entirely — can reach
120
- the exact same toolset, approval-preview logic, and path scoping. The
121
- reverse also holds: any additional MCP server — local (stdio) or remote
122
- (SSE / Streamable HTTP) — can be plugged into this agent, and its tools
123
- merge into the same list the model already sees —
124
- ```bash
125
- coding-agent --add-mcp-server "weather=python -m weather_mcp_server" # local, stdio
126
- coding-agent --add-mcp-server "weather=https://example.com/mcp/sse" # remote, SSE
127
- coding-agent "what's the forecast?" # picked up automatically, every run from here on
128
- ```
129
- A value after `name=` starting with `http://`/`https://` is treated as a
130
- remote server (SSE by default, append `,streamable_http` for that transport
131
- instead); anything else is a local command spawned over stdio — it doesn't
132
- need to be `-m`-invokable, a standalone script's absolute path works too
133
- (e.g. `"myserver=python C:/absolute/path/to/mcp_server.py"`).
134
-
135
- ## Configuration
136
-
137
- Point at any Ollama-compatible host with `--ollama-host` or the
138
- `OLLAMA_HOST` env var. If it sits behind an authenticated proxy, set
139
- `OLLAMA_API_KEY` as an environment variable rather than a CLI flag so the
140
- key doesn't end up in shell history.
141
-
142
- ## Links
143
-
144
- Source, full documentation, and issue tracker:
145
- https://github.com/HarryChen1995/coding_agent
146
-
147
- ## License
148
-
149
- MIT
1
+ Metadata-Version: 2.4
2
+ Name: ollama-coding-agent
3
+ Version: 0.5.4
4
+ Summary: AI coding agent driving Qwen Coder (or any Ollama-compatible model) through a scoped toolset via MCP
5
+ License-Expression: MIT
6
+ Project-URL: Homepage, https://github.com/HarryChen1995/coding_agent
7
+ Project-URL: Repository, https://github.com/HarryChen1995/coding_agent
8
+ Project-URL: Issues, https://github.com/HarryChen1995/coding_agent/issues
9
+ Classifier: Environment :: Console
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Topic :: Software Development :: Build Tools
13
+ Requires-Python: >=3.10
14
+ Description-Content-Type: text/markdown
15
+ License-File: LICENSE
16
+ Requires-Dist: httpx>=0.28
17
+ Requires-Dist: rich>=15.0
18
+ Requires-Dist: typer>=0.27
19
+ Requires-Dist: mcp>=1.27
20
+ Requires-Dist: prompt_toolkit>=3.0
21
+ Dynamic: license-file
22
+
23
+ # Ollama Coding Agent
24
+
25
+ An AI coding agent that plans, edits, and tests code by driving Qwen Coder
26
+ (or any Ollama-compatible model) through a scoped set of file and shell
27
+ tools, with human approval on every write, edit, or shell command.
28
+
29
+ ## Install
30
+
31
+ ```bash
32
+ pip install ollama-coding-agent
33
+ ollama pull qwen3-coder:30b
34
+ ```
35
+
36
+ ## Usage
37
+
38
+ ```bash
39
+ coding-agent "Add type hints to utils.py, then run the test suite" \
40
+ --project-root ./myrepo
41
+ ```
42
+
43
+ Equivalent: `python -m coding_agent "..." --project-root ./myrepo`.
44
+
45
+ Omit the task string to enter an interactive session instead:
46
+
47
+ ```bash
48
+ coding-agent --project-root ./myrepo
49
+ ```
50
+
51
+ Run `coding-agent --help` for the full option list.
52
+
53
+ ## Features
54
+
55
+ - **Structured intent parsing** — the raw task is classified (bug fix,
56
+ feature, refactor, risk level, target files) before any action is taken,
57
+ and high-risk tasks force human approval even under `--auto-approve`.
58
+ - **Session persistence** — every message is saved to SQLite as the run
59
+ happens. Resume a previous run by id or a name you gave it
60
+ (`--resume`), browse saved sessions (`--list-sessions`), or delete one
61
+ (`--delete-session`).
62
+ - **Interactive mode** — drop into a REPL that keeps the model connection
63
+ and tool session alive across turns.
64
+ - **Human-in-the-loop approval** — every write, edit, or shell command
65
+ shows a diff or command preview before you confirm, unless explicitly
66
+ marked safe or run with `--auto-approve`.
67
+ - **Retry and recovery** — transient model failures retry with backoff;
68
+ malformed tool-call output is caught and reported back to the model
69
+ instead of crashing the run.
70
+ - **Codebase exploration tools** — regex content search with glob
71
+ filtering, pattern-based file discovery, directory listing, and git diff,
72
+ all skipping noise directories (`.git`, `node_modules`, build output).
73
+ - **Extensible via custom MCP servers** — point at any MCP server, local
74
+ (stdio) or remote (SSE / Streamable HTTP), and its tools merge into the
75
+ model's toolset automatically, no code changes required. Register one
76
+ permanently (`--add-mcp-server`, available on every future run) or add
77
+ one per run (`--mcp-server`/`--mcp-config`).
78
+
79
+ ## Architecture
80
+
81
+ Tools are served over the Model Context Protocol (MCP), not called
82
+ in-process — the agent is an MCP *client* that talks to a tool server over
83
+ stdio:
84
+
85
+ ```
86
+ +-----------------------------+
87
+ | CLI / REPL |
88
+ +-----------------------------+
89
+ |
90
+ v
91
+ +-----------------------------+
92
+ | Agent loop |
93
+ | parse intent, call model, |
94
+ | approve, execute, persist |
95
+ +-----------------------------+
96
+ |
97
+ v
98
+ +-----------------------------+
99
+ | MCP client |
100
+ | built-in + custom servers |
101
+ | merged into one tool list |
102
+ +-----------------------------+
103
+ |
104
+ stdio / SSE / streamable-http
105
+ v
106
+ +-----------------------------+
107
+ | MCP server(s) |
108
+ +-----------------------------+
109
+ |
110
+ v
111
+ +-----------------------------+
112
+ | Tools |
113
+ | read / write / edit / |
114
+ | search / shell |
115
+ +-----------------------------+
116
+ ```
117
+
118
+ Because tools are exposed over MCP, any MCP-compatible client — Claude
119
+ Desktop, another agent framework, a different model entirely — can reach
120
+ the exact same toolset, approval-preview logic, and path scoping. The
121
+ reverse also holds: any additional MCP server — local (stdio) or remote
122
+ (SSE / Streamable HTTP) — can be plugged into this agent, and its tools
123
+ merge into the same list the model already sees —
124
+ ```bash
125
+ coding-agent --add-mcp-server "weather=python -m weather_mcp_server" # local, stdio
126
+ coding-agent --add-mcp-server "weather=https://example.com/mcp/sse" # remote, SSE
127
+ coding-agent "what's the forecast?" # picked up automatically, every run from here on
128
+ ```
129
+ A value after `name=` starting with `http://`/`https://` is treated as a
130
+ remote server (SSE by default, append `,streamable_http` for that transport
131
+ instead); anything else is a local command spawned over stdio — it doesn't
132
+ need to be `-m`-invokable, a standalone script's absolute path works too
133
+ (e.g. `"myserver=python C:/absolute/path/to/mcp_server.py"`).
134
+
135
+ ## Configuration
136
+
137
+ Point at any Ollama-compatible host with `--ollama-host` or the
138
+ `OLLAMA_HOST` env var. If it sits behind an authenticated proxy, set
139
+ `OLLAMA_API_KEY` as an environment variable rather than a CLI flag so the
140
+ key doesn't end up in shell history.
141
+
142
+ ## Links
143
+
144
+ Source, full documentation, and issue tracker:
145
+ https://github.com/HarryChen1995/coding_agent
146
+
147
+ ## License
148
+
149
+ MIT
@@ -1,35 +1,35 @@
1
- [build-system]
2
- requires = ["setuptools>=68"]
3
- build-backend = "setuptools.build_meta"
4
-
5
- [project]
6
- name = "ollama-coding-agent"
7
- version = "0.5.2"
8
- description = "AI coding agent driving Qwen Coder (or any Ollama-compatible model) through a scoped toolset via MCP"
9
- readme = "PYPI_README.md"
10
- requires-python = ">=3.10"
11
- license = "MIT"
12
- classifiers = [
13
- "Environment :: Console",
14
- "Intended Audience :: Developers",
15
- "Programming Language :: Python :: 3",
16
- "Topic :: Software Development :: Build Tools",
17
- ]
18
- dependencies = [
19
- "httpx>=0.28",
20
- "rich>=15.0",
21
- "typer>=0.27",
22
- "mcp>=1.27",
23
- "prompt_toolkit>=3.0",
24
- ]
25
-
26
- [project.urls]
27
- Homepage = "https://github.com/HarryChen1995/coding_agent"
28
- Repository = "https://github.com/HarryChen1995/coding_agent"
29
- Issues = "https://github.com/HarryChen1995/coding_agent/issues"
30
-
31
- [project.scripts]
32
- coding-agent = "coding_agent.cli:app"
33
-
34
- [tool.setuptools.packages.find]
35
- include = ["coding_agent*"]
1
+ [build-system]
2
+ requires = ["setuptools>=68"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "ollama-coding-agent"
7
+ version = "0.5.4"
8
+ description = "AI coding agent driving Qwen Coder (or any Ollama-compatible model) through a scoped toolset via MCP"
9
+ readme = "PYPI_README.md"
10
+ requires-python = ">=3.10"
11
+ license = "MIT"
12
+ classifiers = [
13
+ "Environment :: Console",
14
+ "Intended Audience :: Developers",
15
+ "Programming Language :: Python :: 3",
16
+ "Topic :: Software Development :: Build Tools",
17
+ ]
18
+ dependencies = [
19
+ "httpx>=0.28",
20
+ "rich>=15.0",
21
+ "typer>=0.27",
22
+ "mcp>=1.27",
23
+ "prompt_toolkit>=3.0",
24
+ ]
25
+
26
+ [project.urls]
27
+ Homepage = "https://github.com/HarryChen1995/coding_agent"
28
+ Repository = "https://github.com/HarryChen1995/coding_agent"
29
+ Issues = "https://github.com/HarryChen1995/coding_agent/issues"
30
+
31
+ [project.scripts]
32
+ coding-agent = "coding_agent.cli:app"
33
+
34
+ [tool.setuptools.packages.find]
35
+ include = ["coding_agent*"]
@@ -1,4 +1,4 @@
1
- [egg_info]
2
- tag_build =
3
- tag_date = 0
4
-
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+