nanoPyCodeAgent 0.1.0__py3-none-any.whl → 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nanopycodeagent/__init__.py +4 -1
- nanopycodeagent/agent.py +174 -0
- {nanopycodeagent-0.1.0.dist-info → nanopycodeagent-0.2.0.dist-info}/METADATA +32 -1
- nanopycodeagent-0.2.0.dist-info/RECORD +7 -0
- {nanopycodeagent-0.1.0.dist-info → nanopycodeagent-0.2.0.dist-info}/WHEEL +1 -1
- nanopycodeagent-0.1.0.dist-info/RECORD +0 -6
- {nanopycodeagent-0.1.0.dist-info → nanopycodeagent-0.2.0.dist-info}/entry_points.txt +0 -0
- {nanopycodeagent-0.1.0.dist-info → nanopycodeagent-0.2.0.dist-info}/licenses/LICENSE +0 -0
nanopycodeagent/__init__.py
CHANGED
nanopycodeagent/agent.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
"""A minimal agent loop built on the Anthropic Python SDK.
|
|
2
|
+
|
|
3
|
+
Run the program, type a message, and Agent replies. The full conversation is
|
|
4
|
+
kept in memory so each turn has context. Type ``/exit`` to quit.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import anthropic
|
|
12
|
+
from anthropic.types import MessageParam
|
|
13
|
+
|
|
14
|
+
# The model used when ANTHROPIC_MODEL is set in neither the environment nor the
|
|
15
|
+
# config file.
|
|
16
|
+
DEFAULT_MODEL = "claude-sonnet-4-6"
|
|
17
|
+
MAX_TOKENS = 8192
|
|
18
|
+
SYSTEM_PROMPT = "You are nanoPyCodeAgent, a concise and helpful coding assistant."
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
# User-level config file. Its ``env`` mapping supplies ANTHROPIC_* values for
|
|
22
|
+
# keys that are not already set in the environment (environment variables win).
|
|
23
|
+
def _default_settings_path() -> Path | None:
|
|
24
|
+
"""Resolve the user-level config path, or ``None`` if home is unknown.
|
|
25
|
+
|
|
26
|
+
``Path.home()`` raises ``RuntimeError`` when the home directory cannot be
|
|
27
|
+
determined (e.g. ``$HOME`` unset and no passwd entry, common in minimal
|
|
28
|
+
containers). Guarding it here keeps ``import nanopycodeagent`` — which runs
|
|
29
|
+
eagerly behind the console script — from crashing at import; a ``None`` path
|
|
30
|
+
simply means "no user config file".
|
|
31
|
+
"""
|
|
32
|
+
try:
|
|
33
|
+
return Path.home() / ".nanoPyCodeAgent" / "settings.json"
|
|
34
|
+
except RuntimeError:
|
|
35
|
+
return None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
SETTINGS_PATH = _default_settings_path()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_settings_env(path: Path | None = None) -> None:
|
|
42
|
+
"""Apply the ``env`` mapping from the config file into ``os.environ``.
|
|
43
|
+
|
|
44
|
+
``path`` defaults to the module-level ``SETTINGS_PATH`` (resolved at call
|
|
45
|
+
time, so it stays overridable). Only ``ANTHROPIC_*`` keys that are not
|
|
46
|
+
already present are set, so environment variables take precedence over the
|
|
47
|
+
config file and unrelated variables are never injected. Behaviour by case:
|
|
48
|
+
|
|
49
|
+
- Missing file, or a home directory that cannot be resolved: silently
|
|
50
|
+
ignored (running without a config file is normal).
|
|
51
|
+
- Unreadable / non-UTF-8 file, malformed JSON, non-object top level, or a
|
|
52
|
+
non-object ``env``: a warning is printed and the file is otherwise
|
|
53
|
+
ignored — a bad config never blocks startup.
|
|
54
|
+
- Empty, whitespace-only, or non-string values, and values the OS rejects
|
|
55
|
+
(e.g. an embedded NUL): skipped (the documented example ships these keys
|
|
56
|
+
as empty-string placeholders).
|
|
57
|
+
"""
|
|
58
|
+
if path is None:
|
|
59
|
+
path = SETTINGS_PATH
|
|
60
|
+
if path is None:
|
|
61
|
+
return # home dir unresolvable → behave as if no config file exists
|
|
62
|
+
try:
|
|
63
|
+
raw = path.read_text(encoding="utf-8")
|
|
64
|
+
except FileNotFoundError:
|
|
65
|
+
return
|
|
66
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
67
|
+
print(f"Warning: could not read config file {path}: {exc}")
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
try:
|
|
71
|
+
data = json.loads(raw)
|
|
72
|
+
except json.JSONDecodeError as exc:
|
|
73
|
+
print(f"Warning: ignoring malformed config file {path}: {exc}")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
if not isinstance(data, dict):
|
|
77
|
+
print(f"Warning: ignoring config file {path}: top level must be an object.")
|
|
78
|
+
return
|
|
79
|
+
|
|
80
|
+
env = data.get("env", {})
|
|
81
|
+
if not isinstance(env, dict):
|
|
82
|
+
print(f"Warning: ignoring 'env' in config file {path}: it must be an object.")
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
for key, value in env.items():
|
|
86
|
+
# Only honor ANTHROPIC_* keys (the config's documented purpose) so a
|
|
87
|
+
# shared settings.json cannot silently inject unrelated variables such
|
|
88
|
+
# as HTTPS_PROXY into the process environment.
|
|
89
|
+
if not key.startswith("ANTHROPIC_"):
|
|
90
|
+
continue
|
|
91
|
+
if not (isinstance(value, str) and value.strip()):
|
|
92
|
+
continue
|
|
93
|
+
try:
|
|
94
|
+
os.environ.setdefault(key, value.strip())
|
|
95
|
+
except ValueError as exc:
|
|
96
|
+
# e.g. an embedded NUL in the value or '=' in the key name.
|
|
97
|
+
print(f"Warning: ignoring invalid config entry {key!r}: {exc}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def run() -> None:
|
|
101
|
+
"""Start the read → ask → answer loop until the user types ``/exit``."""
|
|
102
|
+
# Fill any unset ANTHROPIC_* keys from the config file (environment variables
|
|
103
|
+
# take precedence), then let the SDK read credentials from os.environ.
|
|
104
|
+
load_settings_env()
|
|
105
|
+
client = anthropic.Anthropic()
|
|
106
|
+
if client.api_key is None and client.auth_token is None:
|
|
107
|
+
print(
|
|
108
|
+
"No API credentials found. Set the ANTHROPIC_API_KEY environment variable."
|
|
109
|
+
)
|
|
110
|
+
print(
|
|
111
|
+
"If you use a third-party / proxy service, also set ANTHROPIC_BASE_URL "
|
|
112
|
+
"to point at its endpoint."
|
|
113
|
+
)
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
# Resolve the model after load_settings_env() so a config-file ANTHROPIC_MODEL
|
|
117
|
+
# is honored. An empty or whitespace-only value falls back to the default.
|
|
118
|
+
configured_model = os.environ.get("ANTHROPIC_MODEL", "").strip()
|
|
119
|
+
model = configured_model or DEFAULT_MODEL
|
|
120
|
+
|
|
121
|
+
messages: list[MessageParam] = []
|
|
122
|
+
if configured_model:
|
|
123
|
+
print(f"nanoPyCodeAgent — using model {model} (from ANTHROPIC_MODEL).")
|
|
124
|
+
else:
|
|
125
|
+
print(
|
|
126
|
+
f"nanoPyCodeAgent — using default model {model} "
|
|
127
|
+
"(set ANTHROPIC_MODEL to override)."
|
|
128
|
+
)
|
|
129
|
+
print("Type a message to chat, or /exit to quit.")
|
|
130
|
+
|
|
131
|
+
while True:
|
|
132
|
+
try:
|
|
133
|
+
user_input = input("\nYou> ").strip()
|
|
134
|
+
except (EOFError, KeyboardInterrupt):
|
|
135
|
+
print()
|
|
136
|
+
break
|
|
137
|
+
|
|
138
|
+
if not user_input:
|
|
139
|
+
continue
|
|
140
|
+
if user_input == "/exit":
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
messages.append({"role": "user", "content": user_input})
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
print("\nAgent> ", end="", flush=True)
|
|
147
|
+
message = client.messages.create(
|
|
148
|
+
model=model,
|
|
149
|
+
max_tokens=MAX_TOKENS,
|
|
150
|
+
system=SYSTEM_PROMPT,
|
|
151
|
+
messages=messages,
|
|
152
|
+
)
|
|
153
|
+
text = "".join(b.text for b in message.content if b.type == "text")
|
|
154
|
+
print(text, end="", flush=True)
|
|
155
|
+
print()
|
|
156
|
+
except KeyboardInterrupt:
|
|
157
|
+
# Ctrl-C while waiting on the reply cancels cleanly, mirroring the
|
|
158
|
+
# graceful quit offered at the input prompt.
|
|
159
|
+
print()
|
|
160
|
+
break
|
|
161
|
+
except anthropic.AuthenticationError:
|
|
162
|
+
print(
|
|
163
|
+
"\nAuthentication failed. Check that ANTHROPIC_API_KEY is set correctly."
|
|
164
|
+
)
|
|
165
|
+
break
|
|
166
|
+
except anthropic.APIError as exc:
|
|
167
|
+
print(f"\nRequest failed: {exc}")
|
|
168
|
+
messages.pop() # drop the unanswered user turn so history stays valid
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
# Append the full content blocks so the next turn carries complete context.
|
|
172
|
+
messages.append({"role": "assistant", "content": message.content})
|
|
173
|
+
|
|
174
|
+
print("Bye!")
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nanoPyCodeAgent
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: A nano code agent built from scratch in pure Python.
|
|
5
5
|
Project-URL: Homepage, https://github.com/minixalpha/nanoPyCodeAgent
|
|
6
6
|
Project-URL: Repository, https://github.com/minixalpha/nanoPyCodeAgent
|
|
@@ -13,6 +13,7 @@ Classifier: Intended Audience :: Developers
|
|
|
13
13
|
Classifier: Programming Language :: Python :: 3
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.13
|
|
15
15
|
Requires-Python: >=3.13
|
|
16
|
+
Requires-Dist: anthropic>=0.112.0
|
|
16
17
|
Description-Content-Type: text/markdown
|
|
17
18
|
|
|
18
19
|
# nanoPyCodeAgent
|
|
@@ -60,6 +61,32 @@ uvx --from "git+https://github.com/minixalpha/nanoPyCodeAgent@main" nanoPyCodeAg
|
|
|
60
61
|
uvx --from "git+https://github.com/minixalpha/nanoPyCodeAgent@v0.1.0" nanoPyCodeAgent
|
|
61
62
|
```
|
|
62
63
|
|
|
64
|
+
### Configuration
|
|
65
|
+
|
|
66
|
+
Credentials and the model come from two sources: **environment variables** and
|
|
67
|
+
an optional user-level config file at `~/.nanoPyCodeAgent/settings.json`.
|
|
68
|
+
Environment variables take precedence — the config file only fills in keys you
|
|
69
|
+
have not set in the environment.
|
|
70
|
+
|
|
71
|
+
The config file mirrors [Claude Code's settings](https://code.claude.com/docs/en/settings):
|
|
72
|
+
put the values under an `env` object. Empty or whitespace-only values are ignored.
|
|
73
|
+
|
|
74
|
+
```json
|
|
75
|
+
{
|
|
76
|
+
"env": {
|
|
77
|
+
"ANTHROPIC_API_KEY": "",
|
|
78
|
+
"ANTHROPIC_BASE_URL": "",
|
|
79
|
+
"ANTHROPIC_MODEL": ""
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
| Variable | Required | Default | Description |
|
|
85
|
+
| --- | --- | --- | --- |
|
|
86
|
+
| `ANTHROPIC_API_KEY` | Yes | — | Your Anthropic API key, or the key for a third-party / proxy service. |
|
|
87
|
+
| `ANTHROPIC_BASE_URL` | No | `https://api.anthropic.com` | Point the SDK at a non-official / proxy endpoint. Leave it unset to use the official API — an empty value breaks requests. |
|
|
88
|
+
| `ANTHROPIC_MODEL` | No | `claude-sonnet-4-6` | Override the model. An empty or whitespace-only value falls back to the default. |
|
|
89
|
+
|
|
63
90
|
### How to Update
|
|
64
91
|
|
|
65
92
|
Upgrade an installed tool to the latest release:
|
|
@@ -67,3 +94,7 @@ Upgrade an installed tool to the latest release:
|
|
|
67
94
|
```bash
|
|
68
95
|
uv tool upgrade nanoPyCodeAgent # or: pipx upgrade nanoPyCodeAgent
|
|
69
96
|
```
|
|
97
|
+
|
|
98
|
+
## Releasing
|
|
99
|
+
|
|
100
|
+
For maintainers: see [docs/RELEASING.md](docs/RELEASING.md) for the release process and prerequisites.
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
nanopycodeagent/__init__.py,sha256=pYFCw7WNkZsGXbqNLkXituX4iBy86RJ4LKKAr78XwM8,70
|
|
2
|
+
nanopycodeagent/agent.py,sha256=aAY60px0CuXllg3qICFFOz9_iJKIjf2OWWjmo6PDfzk,6628
|
|
3
|
+
nanopycodeagent-0.2.0.dist-info/METADATA,sha256=etl0E9_jsVOXrRHhTD7lcODGNzSY1VjJ5t-LKxP5adE,3028
|
|
4
|
+
nanopycodeagent-0.2.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
|
|
5
|
+
nanopycodeagent-0.2.0.dist-info/entry_points.txt,sha256=4gU7YNzQaf2RKlOdfebOWYNYIUk2LDoyhu3G4Vgole8,57
|
|
6
|
+
nanopycodeagent-0.2.0.dist-info/licenses/LICENSE,sha256=g24TppoiLdeF_1_-Y7H2H_eOavKseWA5utuK83Vq1sA,1067
|
|
7
|
+
nanopycodeagent-0.2.0.dist-info/RECORD,,
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
nanopycodeagent/__init__.py,sha256=Id0gZ5s6aGlqx-V_x3Eq8ndB99ltn7lN_f-MWHC6kfU,55
|
|
2
|
-
nanopycodeagent-0.1.0.dist-info/METADATA,sha256=LIT9891DoSO6N6OpC8gvn4SNYFkdlXkP7f5Kt9yCuoI,1794
|
|
3
|
-
nanopycodeagent-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
4
|
-
nanopycodeagent-0.1.0.dist-info/entry_points.txt,sha256=4gU7YNzQaf2RKlOdfebOWYNYIUk2LDoyhu3G4Vgole8,57
|
|
5
|
-
nanopycodeagent-0.1.0.dist-info/licenses/LICENSE,sha256=g24TppoiLdeF_1_-Y7H2H_eOavKseWA5utuK83Vq1sA,1067
|
|
6
|
-
nanopycodeagent-0.1.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|