vybthon 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.
vibe/__init__.py ADDED
@@ -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"]
vibe/codegen.py ADDED
@@ -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
vibe/console.py ADDED
@@ -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()
vibe/core.py ADDED
@@ -0,0 +1,200 @@
1
+ """The ``Vibe`` object: undefined methods are synthesized by an LLM on call."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any, Callable
8
+
9
+ import litellm
10
+ from litellm import CustomStreamWrapper, completion
11
+ from litellm.types.utils import ModelResponseStream, StreamingChoices
12
+
13
+ from .codegen import build_messages, materialize, strip_fences
14
+ from .console import Console
15
+
16
+ litellm.suppress_debug_info = True # silence litellm's "Provider List" banner
17
+
18
+
19
+
20
+ class Vibe:
21
+ """An object whose undefined methods are synthesized by an LLM on first call.
22
+
23
+ Call any method name; if it doesn't exist yet, the model writes a function
24
+ that does what the name + arguments imply, it's executed, and the source is
25
+ cached to disk for reuse on later calls and future runs.
26
+
27
+ SECURITY: synthesized code is executed with ``exec``. LLM output is
28
+ untrusted — treat every generated function as arbitrary code. This is a toy,
29
+ not a sandbox; run it only against a local model you trust.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ model: str,
35
+ *,
36
+ api_base: str | None ,
37
+ api_key: str | None = None,
38
+ extra_body: dict[str, Any] | None = None,
39
+ cache_dir: Path | str | None = None,
40
+ verbose: bool = False,
41
+ retries: int = 2,
42
+ caching: bool = True,
43
+ ) -> None:
44
+ self._model = model
45
+ self._api_base = api_base
46
+ self._api_key = api_key
47
+ self._extra_body = extra_body
48
+ self._cache_dir = Path(cache_dir) if cache_dir else Path.cwd() / ".vibe_cache"
49
+ self._retries = retries
50
+ self._console = Console(enabled=verbose)
51
+ self._fns: dict[str, Callable[..., Any]] = {}
52
+ self._cache_dir.mkdir(parents=True, exist_ok=True)
53
+ self._caching = caching
54
+
55
+ @classmethod
56
+ def openrouter(
57
+ cls,
58
+ model: str,
59
+ *,
60
+ reasoning: bool = False,
61
+ **kwargs: Any,
62
+ ) -> "Vibe":
63
+ OPENROUTER_BASE_URL = os.getenv("OPENROUTER_API_BASE", "https://openrouter.ai/api/v1")
64
+ OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY")
65
+
66
+ if reasoning:
67
+ extra_body = {**kwargs.pop("extra_body", {}), "reasoning": {"enabled": True}}
68
+ kwargs["extra_body"] = extra_body
69
+ return cls(model, api_base=OPENROUTER_BASE_URL, api_key=OPENROUTER_API_KEY, **kwargs)
70
+
71
+ # -- public surface ----------------------------------------------------
72
+
73
+ def __getattr__(self, name: str) -> Callable[..., Any]:
74
+ # Never intercept dunder / private lookups (copy, repr, IPython probes,
75
+ # and our own ``self._foo`` access during __init__ all rely on this).
76
+ if name.startswith("_"):
77
+ raise AttributeError(name)
78
+
79
+ def caller(*args: Any, **kwargs: Any) -> Any:
80
+ return self._call(name, args, kwargs)
81
+
82
+ caller.__name__ = name
83
+ caller.__qualname__ = f"Vibe.{name}"
84
+ return caller
85
+
86
+ # -- call / retry ------------------------------------------------------
87
+
88
+ def _call(
89
+ self, name: str, args: tuple[Any, ...], kwargs: dict[str, Any]
90
+ ) -> Any:
91
+ """Resolve and invoke ``name``. If synthesis or the call itself fails,
92
+ evict the bad version and re-synthesize with the exception as context,
93
+ up to ``self._retries`` times."""
94
+ error: Exception | None = None
95
+ for attempt in range(1, self._retries + 2): # 1 initial try + N retries
96
+ try:
97
+ fn = self._resolve(name, args, kwargs, error)
98
+ return fn(*args, **kwargs)
99
+ except Exception as exc:
100
+ self._evict(name) # this version is bad — don't keep or reuse it
101
+ error = exc
102
+ self._console.status(f"{name} failed (attempt {attempt}): {exc!r}")
103
+ assert error is not None
104
+ raise error
105
+
106
+ def _evict(self, name: str) -> None:
107
+ """Drop a synthesized function from both caches so it is regenerated."""
108
+ self._fns.pop(name, None)
109
+ self._cache_path(name).unlink(missing_ok=True)
110
+
111
+ # -- resolution / caching ---------------------------------------------
112
+
113
+ def _resolve(
114
+ self,
115
+ name: str,
116
+ args: tuple[Any, ...],
117
+ kwargs: dict[str, Any],
118
+ error: Exception | None = None,
119
+ ) -> Callable[..., Any]:
120
+ """Return a callable for ``name``: memory cache → disk cache → synthesize.
121
+
122
+ On a retry (``error`` set) the caches are skipped and a fresh version is
123
+ synthesized with the failure as context.
124
+ """
125
+ if self._caching and error is None:
126
+ cached = self._fns.get(name)
127
+ if cached is not None:
128
+ return cached
129
+
130
+ path = self._cache_path(name)
131
+ if path.exists():
132
+ fn = materialize(path.read_text(), name)
133
+ self._fns[name] = fn
134
+ return fn
135
+
136
+ source = strip_fences(self._generate(name, args, kwargs, error))
137
+ fn = materialize(source, name)
138
+ if self._caching:
139
+ self._cache_path(name).write_text(self._cache_header(name) + source + "\n")
140
+ self._fns[name] = fn
141
+ return fn
142
+
143
+ def _cache_path(self, name: str) -> Path:
144
+ return self._cache_dir / f"{name}.py"
145
+
146
+ def _cache_header(self, name: str) -> str:
147
+ return (
148
+ f"# vibe-generated: {name}\n"
149
+ f"# model: {self._model}\n"
150
+ f"# WARNING: machine-written code, review before trusting.\n\n"
151
+ )
152
+
153
+ # -- generation --------------------------------------------------------
154
+
155
+ def _generate(
156
+ self,
157
+ name: str,
158
+ args: tuple[Any, ...],
159
+ kwargs: dict[str, Any],
160
+ error: Exception | None = None,
161
+ ) -> str:
162
+ """Synthesize the source for ``name`` (one request; ``_call`` handles retries).
163
+
164
+ When ``error`` is set, it is passed to the model as context so the new
165
+ version avoids whatever the previous one got wrong.
166
+ """
167
+ self._console.status(f"synthesizing {name}" + (" (retry)" if error else ""))
168
+ source = self._stream(build_messages(name, args, kwargs, error))
169
+ self._console.status(f"synthesized {name}")
170
+ return source
171
+
172
+ def _completion_kwargs(self, messages: list[dict[str, str]]) -> dict[str, Any]:
173
+ """litellm kwargs; api_base/api_key/extra_body sent only when set."""
174
+ kwargs: dict[str, Any] = {
175
+ "model": self._model,
176
+ "messages": messages,
177
+ "stream": True,
178
+ }
179
+ if self._api_base and not self._model.startswith("openrouter/"):
180
+ kwargs["api_base"] = self._api_base
181
+ if self._api_key:
182
+ kwargs["api_key"] = self._api_key
183
+ if self._extra_body:
184
+ kwargs["extra_body"] = self._extra_body
185
+ return kwargs
186
+
187
+ def _stream(self, messages: list[dict[str, str]]) -> str:
188
+ """Stream a completion and return the concatenated content."""
189
+ resp = completion(**self._completion_kwargs(messages))
190
+ assert isinstance(resp, CustomStreamWrapper)
191
+
192
+ parts: list[str] = []
193
+ for chunk in resp:
194
+ assert isinstance(chunk, ModelResponseStream)
195
+ choice = chunk.choices[0]
196
+ assert isinstance(choice, StreamingChoices)
197
+ text = choice.delta.content
198
+ if text:
199
+ parts.append(text)
200
+ return "".join(parts)
@@ -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.
@@ -0,0 +1,7 @@
1
+ vibe/__init__.py,sha256=-4kZSCGn-xRVdCNuAOe29LokI5rkAfy53uMITFosurQ,388
2
+ vibe/codegen.py,sha256=puXMRe1j_DmQ_GFEZwUqZ7RFqqm8XsCYCGpDwj8pkB0,6595
3
+ vibe/console.py,sha256=UJIkCdJeWssCqz5uwJmTZ0QqZFBWpyGLF74qSgKSAdk,635
4
+ vibe/core.py,sha256=2E3CxeCvVlqoc71EuiBBC0QC6DKrdyrVW36pnT_TDA4,7420
5
+ vybthon-0.1.0.dist-info/METADATA,sha256=ViBg0SPs9v0gzJTGAbmzSlpMekRo4SktRvApVLvgpTY,1768
6
+ vybthon-0.1.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
7
+ vybthon-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any