town 0.1.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.
- town/__init__.py +5 -0
- town/__main__.py +3 -0
- town/auth.py +73 -0
- town/blocks.py +73 -0
- town/cell.py +559 -0
- town/cli.py +122 -0
- town/commons.py +554 -0
- town/config.py +117 -0
- town/llm.py +244 -0
- town/memory.py +95 -0
- town/organism.py +862 -0
- town/presets.py +250 -0
- town/prompts.py +230 -0
- town/trace.py +24 -0
- town/viewer.py +339 -0
- town/watch.py +201 -0
- town-0.1.0.dist-info/METADATA +492 -0
- town-0.1.0.dist-info/RECORD +21 -0
- town-0.1.0.dist-info/WHEEL +4 -0
- town-0.1.0.dist-info/entry_points.txt +2 -0
- town-0.1.0.dist-info/licenses/LICENSE +21 -0
town/__init__.py
ADDED
town/__main__.py
ADDED
town/auth.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""`town auth`: interactive onboarding. Asks for the API base URL and key, validates them against the
|
|
2
|
+
models endpoint, and stores them in the user config file. `town auth status` shows what is in effect."""
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import getpass
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
from .config import ANTHROPIC_URL, CONFIG_FILE, Config, load_user_config, save_user_config
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _models(base_url: str, api_key: str) -> list[str]:
|
|
13
|
+
import anthropic
|
|
14
|
+
|
|
15
|
+
client = anthropic.Anthropic(api_key=api_key, base_url=base_url, max_retries=1, timeout=30)
|
|
16
|
+
return [m.id for m in client.models.list()]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def status() -> int:
|
|
20
|
+
cfg = Config()
|
|
21
|
+
src = "env" if os.environ.get("TOWN_API_KEY") or os.environ.get("ANTHROPIC_API_KEY") else "unset"
|
|
22
|
+
if src == "env" and load_user_config().get("TOWN_API_KEY") == os.environ.get("TOWN_API_KEY"):
|
|
23
|
+
src = str(CONFIG_FILE)
|
|
24
|
+
print(f"base url : {cfg.base_url}")
|
|
25
|
+
print(f"api key : {'set (' + cfg.api_key[:8] + '…) from ' + src if cfg.api_key else 'not set'}")
|
|
26
|
+
print(f"model : {cfg.model} helper: {cfg.helper_model}")
|
|
27
|
+
print(f"config : {CONFIG_FILE}{'' if CONFIG_FILE.exists() else ' (absent)'}")
|
|
28
|
+
if not cfg.api_key:
|
|
29
|
+
print("run `town auth` to set up")
|
|
30
|
+
return 1
|
|
31
|
+
try:
|
|
32
|
+
ms = _models(cfg.base_url, cfg.api_key)
|
|
33
|
+
print(f"gateway : ok, {len(ms)} models ({', '.join(ms[:6])}{'…' if len(ms) > 6 else ''})")
|
|
34
|
+
return 0
|
|
35
|
+
except Exception as e: # noqa: BLE001
|
|
36
|
+
print(f"gateway : FAILED ({type(e).__name__}: {str(e)[:160]})")
|
|
37
|
+
return 1
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def onboard(non_interactive: bool = False) -> int:
|
|
41
|
+
print("town setup")
|
|
42
|
+
print("Every citizen calls the Claude API. Tell me where and with which key; env vars override this later.")
|
|
43
|
+
print()
|
|
44
|
+
if non_interactive or not sys.stdin.isatty():
|
|
45
|
+
print("No terminal to ask on. Set TOWN_API_KEY (and TOWN_BASE_URL for a gateway), or run `town auth` in a terminal.")
|
|
46
|
+
return 1
|
|
47
|
+
current = Config()
|
|
48
|
+
default_url = current.base_url or ANTHROPIC_URL
|
|
49
|
+
url = input(f"API base URL [{default_url}] (Anthropic directly, or a gateway such as LiteLLM): ").strip() or default_url
|
|
50
|
+
url = url.rstrip("/")
|
|
51
|
+
key = getpass.getpass("API key (input hidden; sk-ant-… for Anthropic, or your gateway key): ").strip()
|
|
52
|
+
if not key:
|
|
53
|
+
print("no key given; nothing saved")
|
|
54
|
+
return 1
|
|
55
|
+
print("checking…", end=" ", flush=True)
|
|
56
|
+
try:
|
|
57
|
+
ms = _models(url, key)
|
|
58
|
+
except Exception as e: # noqa: BLE001
|
|
59
|
+
print(f"failed: {type(e).__name__}: {str(e)[:200]}")
|
|
60
|
+
print("Nothing saved. Check the URL and key (a LiteLLM gateway takes its own key, not an Anthropic one).")
|
|
61
|
+
return 1
|
|
62
|
+
print(f"ok, {len(ms)} models available")
|
|
63
|
+
default_model = "claude-opus-5" if "claude-opus-5" in ms else (ms[0] if ms else "claude-opus-5")
|
|
64
|
+
model = input(f"default model for citizens [{default_model}]: ").strip() or default_model
|
|
65
|
+
if model not in ms:
|
|
66
|
+
print(f"note: {model!r} is not in the gateway's list; keeping it anyway")
|
|
67
|
+
path = save_user_config({"TOWN_BASE_URL": url, "TOWN_API_KEY": key, "TOWN_MODEL": model, "TOWN_HELPER_MODEL": model})
|
|
68
|
+
print(f"saved to {path} (mode 600)")
|
|
69
|
+
print()
|
|
70
|
+
print("try: town run \"Find out which stdlib module has the most public names. Tell me once.\"")
|
|
71
|
+
print(" town preset fork_merge # one of the built-in experiments")
|
|
72
|
+
print(" town watch traces/<run>.jsonl in a second terminal, to watch it live")
|
|
73
|
+
return 0
|
town/blocks.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""The context data model.
|
|
2
|
+
|
|
3
|
+
A cell's context is NOT a chat transcript. It is an ordered, mutable list of Blocks that gets
|
|
4
|
+
re-rendered into a single document on every model call. Because we do not care about prompt
|
|
5
|
+
caching, every call can see a freshly rewritten context: blocks can be dropped, rewritten,
|
|
6
|
+
pinned, compressed into engrams, recalled from the memory store, or copied into forks.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import itertools
|
|
11
|
+
import time
|
|
12
|
+
from dataclasses import dataclass, field
|
|
13
|
+
|
|
14
|
+
_ids = itertools.count(1)
|
|
15
|
+
|
|
16
|
+
# Default salience per kind. 5 = pinned (never auto-compressed), 1 = first to go.
|
|
17
|
+
DEFAULT_SALIENCE = {
|
|
18
|
+
"task": 5, # the originating task / focus statement
|
|
19
|
+
"user": 5, # something the human said
|
|
20
|
+
"ask": 4, # a question we surfaced to the human
|
|
21
|
+
"msg": 4, # a message from another cell
|
|
22
|
+
"merge": 4, # synthesized result of merging other cells
|
|
23
|
+
"recall": 3, # an engram recalled from memory
|
|
24
|
+
"note": 3, # explicit note written by the cell via ctx.note()
|
|
25
|
+
"system": 3, # harness notices
|
|
26
|
+
"thought": 2, # visible text the model wrote (its monologue)
|
|
27
|
+
"result": 2, # python stdout/stderr/value
|
|
28
|
+
"code": 1, # python code executed
|
|
29
|
+
"think": 1, # summarized thinking (continuous inner voice)
|
|
30
|
+
"stub": 3, # compressed-block placeholder pointing at an engram
|
|
31
|
+
"rewind": 4, # record of a rewind: where from, where to, and why
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class Block:
|
|
37
|
+
kind: str
|
|
38
|
+
text: str
|
|
39
|
+
salience: int = 0
|
|
40
|
+
id: int = field(default_factory=lambda: next(_ids))
|
|
41
|
+
t: float = field(default_factory=time.time)
|
|
42
|
+
step: int = 0
|
|
43
|
+
meta: dict = field(default_factory=dict)
|
|
44
|
+
|
|
45
|
+
def __post_init__(self) -> None:
|
|
46
|
+
if not self.salience:
|
|
47
|
+
self.salience = DEFAULT_SALIENCE.get(self.kind, 3)
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def pinned(self) -> bool:
|
|
51
|
+
return self.salience >= 5
|
|
52
|
+
|
|
53
|
+
def copy(self) -> "Block":
|
|
54
|
+
return Block(kind=self.kind, text=self.text, salience=self.salience, step=self.step, meta=dict(self.meta))
|
|
55
|
+
|
|
56
|
+
def copy_with_id(self) -> "Block":
|
|
57
|
+
b = self.copy()
|
|
58
|
+
b.id, b.t = self.id, self.t
|
|
59
|
+
return b
|
|
60
|
+
|
|
61
|
+
def render(self) -> str:
|
|
62
|
+
attrs = [f"id={self.id}", f"kind={self.kind}", f"s={self.salience}", f"step={self.step}"]
|
|
63
|
+
for k, v in self.meta.items():
|
|
64
|
+
if k in ("from", "engram", "cell", "to", "kept_from"):
|
|
65
|
+
attrs.append(f"{k}={v}")
|
|
66
|
+
return f"<b {' '.join(attrs)}>\n{self.text}\n</b>"
|
|
67
|
+
|
|
68
|
+
def to_json(self) -> dict:
|
|
69
|
+
return {"id": self.id, "kind": self.kind, "s": self.salience, "step": self.step, "text": self.text, "meta": self.meta}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def estimate_tokens(text: str, chars_per_token: float = 3.6) -> int:
|
|
73
|
+
return int(len(text) / chars_per_token) + 1
|