vybthon 0.1.0__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.
- vybthon-0.1.0/.github/workflows/publish.yml +18 -0
- vybthon-0.1.0/.gitignore +16 -0
- vybthon-0.1.0/.python-version +1 -0
- vybthon-0.1.0/PKG-INFO +49 -0
- vybthon-0.1.0/README.md +29 -0
- vybthon-0.1.0/examples/demo.py +74 -0
- vybthon-0.1.0/flake.lock +27 -0
- vybthon-0.1.0/flake.nix +27 -0
- vybthon-0.1.0/pyproject.toml +32 -0
- vybthon-0.1.0/pyrightconfig.json +7 -0
- vybthon-0.1.0/src/vibe/__init__.py +16 -0
- vybthon-0.1.0/src/vibe/codegen.py +181 -0
- vybthon-0.1.0/src/vibe/console.py +22 -0
- vybthon-0.1.0/src/vibe/core.py +200 -0
- vybthon-0.1.0/uv.lock +1411 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
name: publish
|
|
2
|
+
|
|
3
|
+
# Build and publish to PyPI when you publish a GitHub Release.
|
|
4
|
+
on:
|
|
5
|
+
release:
|
|
6
|
+
types: [published]
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
pypi:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
environment: pypi # must match the Trusted Publisher "Environment name"
|
|
12
|
+
permissions:
|
|
13
|
+
id-token: write # required for PyPI trusted publishing (OIDC)
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v6
|
|
17
|
+
- run: uv build # writes sdist + wheel to dist/
|
|
18
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
vybthon-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.13
|
vybthon-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: vybthon
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Objects whose methods are written by an LLM the moment you call them
|
|
5
|
+
Project-URL: Homepage, https://github.com/bhivam/vybthon
|
|
6
|
+
Project-URL: Repository, https://github.com/bhivam/vybthon
|
|
7
|
+
Author-email: bhivam <shivamkajaria@gmail.com>
|
|
8
|
+
Keywords: codegen,litellm,llm,metaprogramming,openrouter
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development :: Code Generators
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: litellm>=1.88.1
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# vybthon
|
|
22
|
+
|
|
23
|
+
Objects whose methods are written by an LLM the moment you call them. Call any
|
|
24
|
+
method; if it doesn't exist, a model writes it from the name + arguments, runs
|
|
25
|
+
it, and caches the source to `./.vibe_cache/` for instant reuse.
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from vibe import Vibe
|
|
29
|
+
|
|
30
|
+
v = Vibe.openrouter("openrouter/meta-llama/llama-3.1-8b-instruct") # set OPENROUTER_API_KEY
|
|
31
|
+
v.reverse_string("hello") # -> "olleh"
|
|
32
|
+
v.nth_prime(10) # cached after first call -> 29
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
Needs Python ≥ 3.10 and an LLM provider (hosted, or a local Ollama).
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install "git+https://github.com/bhivam/vybthon" # or: uv add "git+..."
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The distribution is `vybthon`; the import is `vibe`. On NixOS, run inside
|
|
44
|
+
`nix develop` (litellm's wheels need `libstdc++`).
|
|
45
|
+
|
|
46
|
+
## Security
|
|
47
|
+
|
|
48
|
+
Synthesized code runs via `exec`. LLM output is untrusted — treat every
|
|
49
|
+
generated function as arbitrary code. This is a toy, not a sandbox.
|
vybthon-0.1.0/README.md
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# vybthon
|
|
2
|
+
|
|
3
|
+
Objects whose methods are written by an LLM the moment you call them. Call any
|
|
4
|
+
method; if it doesn't exist, a model writes it from the name + arguments, runs
|
|
5
|
+
it, and caches the source to `./.vibe_cache/` for instant reuse.
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from vibe import Vibe
|
|
9
|
+
|
|
10
|
+
v = Vibe.openrouter("openrouter/meta-llama/llama-3.1-8b-instruct") # set OPENROUTER_API_KEY
|
|
11
|
+
v.reverse_string("hello") # -> "olleh"
|
|
12
|
+
v.nth_prime(10) # cached after first call -> 29
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Install
|
|
16
|
+
|
|
17
|
+
Needs Python ≥ 3.10 and an LLM provider (hosted, or a local Ollama).
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install "git+https://github.com/bhivam/vybthon" # or: uv add "git+..."
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The distribution is `vybthon`; the import is `vibe`. On NixOS, run inside
|
|
24
|
+
`nix develop` (litellm's wheels need `libstdc++`).
|
|
25
|
+
|
|
26
|
+
## Security
|
|
27
|
+
|
|
28
|
+
Synthesized code runs via `exec`. LLM output is untrusted — treat every
|
|
29
|
+
generated function as arbitrary code. This is a toy, not a sandbox.
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Prototype: a contact-list importer that cleans messy exported data.
|
|
2
|
+
|
|
3
|
+
A familiar chore — you exported contacts from somewhere and the rows are a mess:
|
|
4
|
+
inconsistent name casing, five different phone formats, mixed-case emails, the
|
|
5
|
+
odd broken address. This prototype normalizes and validates every row, then
|
|
6
|
+
prints a tidy report plus a breakdown by email provider.
|
|
7
|
+
|
|
8
|
+
The interesting part: none of the cleaning or validation logic is written by
|
|
9
|
+
hand. Each ``v.<something>(...)`` call is a function that doesn't exist yet —
|
|
10
|
+
the model writes it from the name and argument types on first call, it runs, and
|
|
11
|
+
the source is cached to ./.vibe_cache/ for instant reuse afterwards.
|
|
12
|
+
|
|
13
|
+
Run inside `nix develop` (so libstdc++ is on LD_LIBRARY_PATH) with an
|
|
14
|
+
OPENROUTER_API_KEY in your environment:
|
|
15
|
+
|
|
16
|
+
uv run python examples/demo.py
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from collections import Counter
|
|
20
|
+
|
|
21
|
+
from vibe import Vibe
|
|
22
|
+
|
|
23
|
+
RAW_CONTACTS = [
|
|
24
|
+
{"name": " ada LOVELACE ", "phone": "(415) 555-0132", "email": "Ada@Gmail.com"},
|
|
25
|
+
{"name": "alan turing", "phone": "415.555.0199", "email": "alan.turing@bletchley.uk"},
|
|
26
|
+
{"name": "GRACE hopper", "phone": "+1 415 555 0150", "email": "grace@navy.mil"},
|
|
27
|
+
{"name": "katherine Johnson", "phone": "4155550177", "email": "not-an-email"},
|
|
28
|
+
{"name": "linus torvalds", "phone": "415-555-0164", "email": "Linus@gmail.com"},
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def main() -> None:
|
|
33
|
+
v = Vibe.openrouter(
|
|
34
|
+
model="openrouter/meta-llama/Llama-3.1-8B-Instruct",
|
|
35
|
+
verbose=True,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
providers: Counter[str] = Counter()
|
|
39
|
+
rows: list[tuple[str, str, str, bool]] = []
|
|
40
|
+
|
|
41
|
+
for raw in RAW_CONTACTS:
|
|
42
|
+
name = v.normalize_full_name(raw["name"])
|
|
43
|
+
phone = v.format_phone_number_e164(raw["phone"], "US")
|
|
44
|
+
email = raw["email"].strip().lower()
|
|
45
|
+
valid = v.is_valid_email_address(email)
|
|
46
|
+
|
|
47
|
+
if valid:
|
|
48
|
+
shown_email = v.mask_email_address(email)
|
|
49
|
+
provider = v.email_provider_from_address(email)
|
|
50
|
+
if provider: # synthesized code may return None on odd input
|
|
51
|
+
providers[provider] += 1
|
|
52
|
+
else:
|
|
53
|
+
shown_email = "<invalid>"
|
|
54
|
+
|
|
55
|
+
rows.append((name, phone, shown_email, valid))
|
|
56
|
+
|
|
57
|
+
# --- cleaned report -------------------------------------------------
|
|
58
|
+
print(f"\n{'NAME':<20}{'PHONE':<18}{'EMAIL':<22}STATUS")
|
|
59
|
+
print("-" * 66)
|
|
60
|
+
for name, phone, email, valid in rows:
|
|
61
|
+
# str(): synthesized functions may return None, which can't be formatted.
|
|
62
|
+
status = "ok" if valid else "REVIEW"
|
|
63
|
+
print(f"{str(name):<20}{str(phone):<18}{str(email):<22}{status}")
|
|
64
|
+
|
|
65
|
+
flagged = sum(1 for *_, valid in rows if not valid)
|
|
66
|
+
print(f"\n{len(rows)} contacts imported, {flagged} flagged for review")
|
|
67
|
+
|
|
68
|
+
print("\ncontacts by email provider:")
|
|
69
|
+
for provider, count in providers.most_common():
|
|
70
|
+
print(f" {provider:<12} {count}")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
main()
|
vybthon-0.1.0/flake.lock
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"nodes": {
|
|
3
|
+
"nixpkgs": {
|
|
4
|
+
"locked": {
|
|
5
|
+
"lastModified": 1780749050,
|
|
6
|
+
"narHash": "sha256-3av0pIjlOWQ6rDbNOmpUSvbNnJkGORQKKjb4LtCZsIY=",
|
|
7
|
+
"owner": "nixos",
|
|
8
|
+
"repo": "nixpkgs",
|
|
9
|
+
"rev": "a799d3e3886da994fa307f817a6bc705ae538eeb",
|
|
10
|
+
"type": "github"
|
|
11
|
+
},
|
|
12
|
+
"original": {
|
|
13
|
+
"owner": "nixos",
|
|
14
|
+
"ref": "nixos-unstable",
|
|
15
|
+
"repo": "nixpkgs",
|
|
16
|
+
"type": "github"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"root": {
|
|
20
|
+
"inputs": {
|
|
21
|
+
"nixpkgs": "nixpkgs"
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
},
|
|
25
|
+
"root": "root",
|
|
26
|
+
"version": 7
|
|
27
|
+
}
|
vybthon-0.1.0/flake.nix
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
inputs = {
|
|
3
|
+
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
outputs = { self, nixpkgs }:
|
|
7
|
+
let
|
|
8
|
+
supportedSystems = [ "x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin" ];
|
|
9
|
+
forEachSupportedSystem = f: nixpkgs.lib.genAttrs supportedSystems (system: f {
|
|
10
|
+
pkgs = import nixpkgs { inherit system; };
|
|
11
|
+
});
|
|
12
|
+
in
|
|
13
|
+
{
|
|
14
|
+
devShells = forEachSupportedSystem ({ pkgs }: {
|
|
15
|
+
default = pkgs.mkShell {
|
|
16
|
+
packages = with pkgs; [
|
|
17
|
+
python3
|
|
18
|
+
uv
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
# Precompiled Python wheels (e.g. litellm's `tokenizers`) link
|
|
22
|
+
# against a standard libstdc++; expose it so they load at runtime.
|
|
23
|
+
LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ pkgs.stdenv.cc.cc.lib ];
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
};
|
|
27
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "vybthon"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Objects whose methods are written by an LLM the moment you call them"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
authors = [{ name = "bhivam", email = "shivamkajaria@gmail.com" }]
|
|
8
|
+
keywords = ["llm", "codegen", "metaprogramming", "litellm", "openrouter"]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Programming Language :: Python :: 3.10",
|
|
14
|
+
"Programming Language :: Python :: 3.11",
|
|
15
|
+
"Programming Language :: Python :: 3.12",
|
|
16
|
+
"Programming Language :: Python :: 3.13",
|
|
17
|
+
"Topic :: Software Development :: Code Generators",
|
|
18
|
+
]
|
|
19
|
+
dependencies = [
|
|
20
|
+
"litellm>=1.88.1",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://github.com/bhivam/vybthon"
|
|
25
|
+
Repository = "https://github.com/bhivam/vybthon"
|
|
26
|
+
|
|
27
|
+
[build-system]
|
|
28
|
+
requires = ["hatchling"]
|
|
29
|
+
build-backend = "hatchling.build"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/vibe"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""vibe: objects whose methods are written by an LLM the moment you call them
|
|
2
|
+
|
|
3
|
+
>>> from vibe import Vibe
|
|
4
|
+
>>> v = Vibe()
|
|
5
|
+
>>> v.reverse_string("hello")
|
|
6
|
+
'olleh'
|
|
7
|
+
>>> v.nth_prime(10)
|
|
8
|
+
29
|
|
9
|
+
|
|
10
|
+
vibed code is executed with minimal checks. LLM output is untrusted.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from .codegen import VibeError
|
|
14
|
+
from .core import Vibe
|
|
15
|
+
|
|
16
|
+
__all__ = ["Vibe", "VibeError"]
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Any, Callable
|
|
5
|
+
|
|
6
|
+
_FENCE_RE = re.compile(r"```(?:[a-zA-Z0-9_+-]*)?\s*\n(.*?)```", re.DOTALL)
|
|
7
|
+
|
|
8
|
+
# How much of each argument to show the model: enough to convey shape and
|
|
9
|
+
# element types without flooding the prompt with large inputs.
|
|
10
|
+
_SAMPLE = 3 # elements shown from a collection argument
|
|
11
|
+
_MAX_REPR = 200 # characters shown from a single value's repr
|
|
12
|
+
|
|
13
|
+
SYSTEM_PROMPT = """\
|
|
14
|
+
You are a Python function synthesizer. Given a function name and the exact \
|
|
15
|
+
arguments it will be called with, you write exactly one Python function \
|
|
16
|
+
implementing the behavior implied by the name.
|
|
17
|
+
|
|
18
|
+
Hard rules:
|
|
19
|
+
- Output ONLY the function definition. No prose, no markdown fences, no usage \
|
|
20
|
+
examples, no test calls, no print statements.
|
|
21
|
+
- The function MUST be named exactly `{name}`.
|
|
22
|
+
- The function MUST be callable exactly as the call shown below. Every parameter \
|
|
23
|
+
the call does not supply MUST have a default value, so that call succeeds. Extra \
|
|
24
|
+
optional parameters are fine; an extra *required* parameter is not. Add precise \
|
|
25
|
+
type hints.
|
|
26
|
+
- Add one concise docstring line.
|
|
27
|
+
- Use ONLY the Python standard library. Third-party packages (e.g. \
|
|
28
|
+
`phonenumbers`, `numpy`, `requests`) are NOT installed and importing them fails \
|
|
29
|
+
at runtime — implement the behavior yourself instead. Put any `import` \
|
|
30
|
+
statements INSIDE the function body.
|
|
31
|
+
- Be pure and deterministic: no I/O, no network, no global state, and no \
|
|
32
|
+
randomness unless the name explicitly implies it.
|
|
33
|
+
- Return the result; never just print it.\
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
USER_PROMPT = """\
|
|
37
|
+
Write the function `{name}`.
|
|
38
|
+
|
|
39
|
+
It will be called like:
|
|
40
|
+
{call}
|
|
41
|
+
|
|
42
|
+
Arguments:
|
|
43
|
+
{arguments}
|
|
44
|
+
|
|
45
|
+
Define `{name}` so this exact call works, then infer the intended behavior from \
|
|
46
|
+
the name and the inputs and return the function definition.\
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
RETRY_PROMPT = """\
|
|
50
|
+
|
|
51
|
+
The previous version of `{name}` failed when called with these arguments:
|
|
52
|
+
{error}
|
|
53
|
+
|
|
54
|
+
Write a corrected version that does not raise this error.{hint}\
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
# Extra, error-specific guidance appended to the retry prompt.
|
|
58
|
+
IMPORT_HINT = (
|
|
59
|
+
" That module is NOT installed and never will be — do not reach for another "
|
|
60
|
+
"third-party package. Implement this using ONLY the Python standard library."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
class VibeError(RuntimeError):
|
|
65
|
+
"""Raised when synthesis produces something we can't turn into a function.
|
|
66
|
+
|
|
67
|
+
Carries the offending source; ``str(err)`` appends it with line numbers.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self, message: str, *, name: str | None = None, source: str | None = None
|
|
72
|
+
) -> None:
|
|
73
|
+
super().__init__(message)
|
|
74
|
+
self.name = name
|
|
75
|
+
self.source = source
|
|
76
|
+
|
|
77
|
+
def __str__(self) -> str:
|
|
78
|
+
base = super().__str__()
|
|
79
|
+
if not self.source:
|
|
80
|
+
return base
|
|
81
|
+
width = len(str(self.source.count("\n") + 1))
|
|
82
|
+
numbered = "\n".join(
|
|
83
|
+
f" {i:>{width}} | {line}"
|
|
84
|
+
for i, line in enumerate(self.source.splitlines(), 1)
|
|
85
|
+
)
|
|
86
|
+
label = f" for {self.name!r}" if self.name else ""
|
|
87
|
+
return f"{base}\n\n synthesized source{label}:\n{numbered}"
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def strip_fences(text: str) -> str:
|
|
91
|
+
"""Pull code out of a ```fenced``` block if present, else return as-is."""
|
|
92
|
+
match = _FENCE_RE.search(text)
|
|
93
|
+
return (match.group(1) if match else text).strip()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _preview(value: Any) -> str:
|
|
97
|
+
"""A bounded repr of ``value``: collections sampled, long values truncated."""
|
|
98
|
+
if isinstance(value, (list, tuple, set, frozenset)):
|
|
99
|
+
items = list(value)
|
|
100
|
+
shown = ", ".join(repr(x) for x in items[:_SAMPLE])
|
|
101
|
+
more = f", … (+{len(items) - _SAMPLE} more)" if len(items) > _SAMPLE else ""
|
|
102
|
+
brackets = {list: "[]", tuple: "()", set: "{}", frozenset: "{}"}[type(value)]
|
|
103
|
+
return f"{brackets[0]}{shown}{more}{brackets[1]}"
|
|
104
|
+
if isinstance(value, dict):
|
|
105
|
+
items = list(value.items())
|
|
106
|
+
shown = ", ".join(f"{k!r}: {v!r}" for k, v in items[:_SAMPLE])
|
|
107
|
+
more = f", … (+{len(items) - _SAMPLE} more)" if len(items) > _SAMPLE else ""
|
|
108
|
+
return f"{{{shown}{more}}}"
|
|
109
|
+
text = repr(value)
|
|
110
|
+
if len(text) > _MAX_REPR:
|
|
111
|
+
return f"{text[:_MAX_REPR]}… (+{len(text) - _MAX_REPR} chars)"
|
|
112
|
+
return text
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def render_call(name: str, args: tuple[Any, ...], kwargs: dict[str, Any]) -> str:
|
|
116
|
+
"""Render the call expression, e.g. ``greet('Sam', loud=True)`` (values bounded)."""
|
|
117
|
+
parts = [_preview(a) for a in args]
|
|
118
|
+
parts += [f"{k}={_preview(v)}" for k, v in kwargs.items()]
|
|
119
|
+
return f"{name}({', '.join(parts)})"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def render_arguments(args: tuple[Any, ...], kwargs: dict[str, Any]) -> str:
|
|
123
|
+
"""One line per argument: position/name, type, and a bounded value preview."""
|
|
124
|
+
lines = [
|
|
125
|
+
f" - positional #{i}: type {type(a).__name__}, value {_preview(a)}"
|
|
126
|
+
for i, a in enumerate(args, 1)
|
|
127
|
+
]
|
|
128
|
+
lines += [
|
|
129
|
+
f" - keyword {k!r}: type {type(v).__name__}, value {_preview(v)}"
|
|
130
|
+
for k, v in kwargs.items()
|
|
131
|
+
]
|
|
132
|
+
return "\n".join(lines) if lines else " (none)"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def build_messages(
|
|
136
|
+
name: str,
|
|
137
|
+
args: tuple[Any, ...],
|
|
138
|
+
kwargs: dict[str, Any],
|
|
139
|
+
error: Exception | None = None,
|
|
140
|
+
) -> list[dict[str, str]]:
|
|
141
|
+
"""Build the chat messages that ask the model to synthesize ``name``.
|
|
142
|
+
|
|
143
|
+
If ``error`` is given (a previous attempt that raised), its type and message
|
|
144
|
+
are appended so the model can correct course on the retry.
|
|
145
|
+
"""
|
|
146
|
+
user = USER_PROMPT.format(
|
|
147
|
+
name=name,
|
|
148
|
+
call=render_call(name, args, kwargs),
|
|
149
|
+
arguments=render_arguments(args, kwargs),
|
|
150
|
+
)
|
|
151
|
+
if error is not None:
|
|
152
|
+
hint = IMPORT_HINT if isinstance(error, ImportError) else ""
|
|
153
|
+
user += RETRY_PROMPT.format(
|
|
154
|
+
name=name, error=f"{type(error).__name__}: {error}", hint=hint
|
|
155
|
+
)
|
|
156
|
+
return [
|
|
157
|
+
{"role": "system", "content": SYSTEM_PROMPT.format(name=name)},
|
|
158
|
+
{"role": "user", "content": user},
|
|
159
|
+
]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def materialize(source: str, name: str) -> Callable[..., Any]:
|
|
163
|
+
"""exec ``source`` in a fresh namespace and pull out the function ``name``."""
|
|
164
|
+
namespace: dict[str, Any] = {}
|
|
165
|
+
try:
|
|
166
|
+
exec(compile(source, f"<vibe:{name}>", "exec"), namespace)
|
|
167
|
+
except SyntaxError as exc:
|
|
168
|
+
raise VibeError(
|
|
169
|
+
f"synthesized code for {name!r} is not valid Python: {exc}",
|
|
170
|
+
name=name,
|
|
171
|
+
source=source,
|
|
172
|
+
) from exc
|
|
173
|
+
fn = namespace.get(name)
|
|
174
|
+
if not callable(fn):
|
|
175
|
+
raise VibeError(
|
|
176
|
+
f"synthesized code did not define a callable named {name!r}; "
|
|
177
|
+
f"got {type(fn).__name__}",
|
|
178
|
+
name=name,
|
|
179
|
+
source=source,
|
|
180
|
+
)
|
|
181
|
+
return fn
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Minimal status output for synthesis. Colour is used only on a TTY."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
_DIM = "\033[2m"
|
|
8
|
+
_RESET = "\033[0m"
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class Console:
|
|
12
|
+
def __init__(self, *, enabled: bool = False) -> None:
|
|
13
|
+
self._on = enabled
|
|
14
|
+
self._color = enabled and sys.stderr.isatty()
|
|
15
|
+
|
|
16
|
+
def status(self, message: str) -> None:
|
|
17
|
+
"""A dim, prefixed status line, e.g. ``vibe synthesizing reverse_string``."""
|
|
18
|
+
if not self._on:
|
|
19
|
+
return
|
|
20
|
+
line = f"vibe {message}"
|
|
21
|
+
sys.stderr.write(f"{_DIM}{line}{_RESET}\n" if self._color else f"{line}\n")
|
|
22
|
+
sys.stderr.flush()
|