Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
{
"python-envs.pythonProjects": []
"python-envs.pythonProjects": [],
"python-envs.defaultEnvManager": "ms-python.python:venv",
"python-envs.defaultPackageManager": "ms-python.python:pip"
}
179 changes: 157 additions & 22 deletions AGENTS.md

Large diffs are not rendered by default.

157 changes: 135 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# wishful πŸͺ„

[![PyPI version](https://badge.fury.io/py/wishful.svg)](https://badge.fury.io/py/wishful)
[![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
[![Tests](https://img.shields.io/badge/tests-83%20passed-brightgreen.svg)](https://github.com/pyros-projects/wishful)
[![Coverage](https://img.shields.io/badge/coverage-80%25-green.svg)](https://github.com/pyros-projects/wishful)
[![Code style: ruff](https://img.shields.io/badge/code%20style-ruff-000000.svg)](https://github.com/astral-sh/ruff)

> _"Code so good, you'd think it was wishful thinking"_

Stop writing boilerplate. Start wishing for it instead.
Expand Down Expand Up @@ -44,8 +51,8 @@ or any provider else supported by litellm
**3. Import your wildest fantasies**

```python
from wishful.text import extract_emails
from wishful.dates import to_yyyy_mm_dd
from wishful.static.text import extract_emails
from wishful.static.dates import to_yyyy_mm_dd

raw = "Contact us at team@example.com or sales@demo.dev"
print(extract_emails(raw)) # ['team@example.com', 'sales@demo.dev']
Expand All @@ -59,6 +66,8 @@ print(to_yyyy_mm_dd("31.12.2025")) # '2025-12-31'

It's like having a junior dev who never sleeps and always delivers exactly what you asked for (well, _almost_ always).

> **Note**: Use `wishful.static.*` for cached imports (recommended) or `wishful.dynamic.*` for runtime-aware regeneration on every import. See [Static vs Dynamic](#-static-vs-dynamic-when-to-use-which) below.

---

## 🎯 Wishful Guidance: Help the AI Read Your Mind
Expand All @@ -67,7 +76,7 @@ Want better results? Drop hints. Literal comments. wishful reads the code _aroun

```python
# desired: parse standard nginx combined logs into list of dicts
from wishful.logs import parse_nginx_logs
from wishful.static.logs import parse_nginx_logs

records = parse_nginx_logs(Path("/var/log/nginx/access.log").read_text())
```
Expand All @@ -76,6 +85,96 @@ The AI sees your comment and knows _exactly_ what you're after. It's like pair p

---

## 🎨 Type Registry: Teach the AI Your Data Structures

Want the LLM to generate functions that return **properly structured data**? Register your types with `@wishful.type`:

### Pydantic Models with Constraints

```python
from pydantic import BaseModel, Field
import wishful

@wishful.type
class ProjectPlan(BaseModel):
"""Project plan written by master yoda from star wars."""
project_brief: str
milestones: list[str] = Field(description="list of milestones", min_length=10)
budget: float = Field(gt=0, description="project budget in USD")

# Now the LLM knows about ProjectPlan and will respect Field constraints!
from wishful.static.pm import project_plan_generator

plan = project_plan_generator(idea="sudoku web app")
print(plan.milestones)
# ['Decide, you must, key features.', 'Wireframe, you will, the interface.', ...]
# ^ 10+ milestones in Yoda-speak because of the docstring! 🎭
```

**What's happening here?**
- The `@wishful.type` decorator registers your Pydantic model
- The **docstring** influences the LLM's tone/style (Yoda-speak!)
- **Field constraints** (`min_length=10`, `gt=0`) are actually enforced
- Generated code uses your exact type definition

### Dataclasses and TypedDict Too

```python
from dataclasses import dataclass
from typing import TypedDict

@wishful.type(output_for="parse_user_data")
@dataclass
class UserProfile:
"""User profile with name, email, and age."""
name: str
email: str
age: int

class ProductInfo(TypedDict):
"""Product information."""
name: str
price: float
in_stock: bool

# Tell the LLM multiple functions use this type
wishful.type(ProductInfo, output_for=["parse_product", "create_product"])
```

The LLM will generate functions that return instances of your registered types. It's like having an API contract, but the implementation writes itself. ✨

---

## πŸ”„ Static vs Dynamic: When to Use Which

wishful supports two import modes:

### `wishful.static.*` β€” Cached & Consistent (Default)

```python
from wishful.static.text import extract_emails
```

- βœ… **Cached**: Generated once, reused forever
- βœ… **Fast**: No LLM calls after first import
- βœ… **Editable**: Tweak `.wishful/text.py` directly
- πŸ‘‰ **Use for**: utilities, parsers, validators, anything stable

### `wishful.dynamic.*` β€” Runtime-Aware & Fresh

```python
from wishful.dynamic.content import generate_story
```

- πŸ”„ **Regenerates**: Fresh LLM call on every import
- 🎯 **Context-aware**: Captures runtime context each time
- 🎨 **Creative**: Different results on each run
- πŸ‘‰ **Use for**: creative content, experiments, testing variations

**Note**: Dynamic imports always regenerate and never use the cache, even if a cached version exists. This ensures fresh, context-aware results every time.

---

## πŸ—„οΈ Cache Ops: Because Sometimes Wishes Need Revising

### Python API
Expand All @@ -87,7 +186,7 @@ import wishful
wishful.inspect_cache() # ['.wishful/text.py', '.wishful/dates.py']

# Regret a wish? Regenerate it
wishful.regenerate("wishful.text") # Next import re-generates from scratch
wishful.regenerate("wishful.static.text") # Next import re-generates from scratch

# Nuclear option: forget everything
wishful.clear_cache() # Deletes the entire .wishful/ directory
Expand All @@ -105,7 +204,7 @@ wishful inspect
wishful clear

# Regenerate a specific module
wishful regen wishful.text
wishful regen wishful.static.text
```

The cache is just regular Python files in `.wishful/`. Want to tweak the generated code? Edit it directly. It's your wish, after all.
Expand All @@ -122,10 +221,7 @@ wishful.configure(
cache_dir="/tmp/.wishful", # Hide your wishes somewhere else
spinner=False, # Silence the "generating..." spinner
review=True, # Paranoid? Review code before it runs
# Control how much nearby code is sent to the LLM for context
# (applies to both import lines and call sites)
# or set via WISHFUL_CONTEXT_RADIUS env var.
# Example: wishful.set_context_radius(6)
context_radius=6, # Lines of context around imports/calls (default: 3)
allow_unsafe=False, # Keep the safety rails ON (recommended)
)
```
Expand Down Expand Up @@ -179,22 +275,22 @@ python my_tests.py # No API calls, just predictable stubs

Here's the 30-second version:

1. **Import hook**: wishful installs a `MagicFinder` on `sys.meta_path` that intercepts `wishful.*` imports.
2. **Cache check**: If `.wishful/<module>.py` exists, it loads instantly. No AI needed.
3. **LLM generation**: If not cached, wishful calls the LLM (via `litellm`) to generate the code based on your import and surrounding context.
4. **Validation**: The generated code is AST-parsed and safety-checked (unless you disabled that like a madman).
5. **Execution**: Code is written to `.wishful/`, compiled, and executed as the import result.
6. **Transparency**: The cache is just plain Python files. Edit them. Commit them. They're yours.
1. **Import hook**: wishful installs a `MagicFinder` on `sys.meta_path` that intercepts `wishful.static.*` and `wishful.dynamic.*` imports.
2. **Cache check**: For `static` imports, if `.wishful/<module>.py` exists, it loads instantly. `dynamic` imports always regenerate.
3. **Context discovery**: wishful captures nearby comments, code, and registered type schemas to send to the LLM.
4. **LLM generation**: The LLM (via `litellm`) generates code based on your import, context, and type definitions.
5. **Validation**: The generated code is AST-parsed and safety-checked (unless you disabled that like a madman).
6. **Execution**: Code is written to `.wishful/`, compiled, and executed as the import result.
7. **Transparency**: The cache is just plain Python files. Edit them. Commit them. They're yours.

It's import hooks meets LLMs meets "why didn't this exist already?"
It's import hooks meets LLMs meets type-aware code generation meets "why didn't this exist already?"

---

## 🎭 Fun with Wishful Thinking

```python
# Need some cosmic horror? Just wish for it.
from wishful.story import cosmic_horror_intro
# Need some cosatic.story import cosmic_horror_intro

intro = cosmic_horror_intro(
setting="a deserted amusement park",
Expand All @@ -203,14 +299,20 @@ intro = cosmic_horror_intro(
print(intro) # πŸŽ’πŸ‘»

# Math that writes itself
from wishful.numbers import primes_from_to, sum_list
from wishful.static.numbers import primes_from_to, sum_list

total = sum_list(list=primes_from_to(1, 100))
print(total) # 1060 (probably)

# Because who has time to write date parsers?
from wishful.dates import parse_fuzzy_date
from wishful.static.dates import parse_fuzzy_date

print(parse_fuzzy_date("next Tuesday")) # Your guess is as good as mine

# Want different results each time? Use dynamic imports!
from wishful.dynamic.jokes import programming_joke

print(programming_joke()) # New joke on every import 🎲
print(parse_fuzzy_date("next Tuesday")) # Your guess is as good as mine
```

Expand Down Expand Up @@ -281,11 +383,15 @@ wishful/
β”‚ β”œβ”€β”€ __main__.py # CLI interface
β”‚ β”œβ”€β”€ config.py # Configuration
β”‚ β”œβ”€β”€ cache/ # Cache management
β”‚ β”œβ”€β”€ core/ # Import hooks
β”‚ β”œβ”€β”€ core/ # Import hooks & discovery
β”‚ β”œβ”€β”€ llm/ # LLM integration
β”‚ β”œβ”€β”€ types/ # Type registry system
β”‚ └── safety/ # Safety validation
β”œβ”€β”€ tests/ # Test suite
β”œβ”€β”€ tests/ # Test suite (83 tests, 80% coverage)
β”œβ”€β”€ examples/ # Usage examples
β”‚ β”œβ”€β”€ 07_typed_outputs.py # Type registry showcase
β”‚ β”œβ”€β”€ 08_dynamic_vs_static.py # Static vs dynamic modes
β”‚ └── 09_context_shenanigans.py # Context discovery
└── pyproject.toml # Project config
```

Expand All @@ -296,6 +402,13 @@ wishful/
**Q: Is this production-ready?**
A: Define "production." πŸ™ƒ

**Q: Can I make the LLM follow a specific style?**
A: Yes! Use docstrings in `@wishful.type` decorated classes. Want Yoda-speak? Add `"""Written by master yoda from star wars."""` β€” the LLM will actually do it.

**Q: Do type hints and Pydantic constraints actually work?**
A: Surprisingly, yes! Field constraints like `min_length=10` or `gt=0` are serialized and sent to the LLM, which respects them.


**Q: What if the LLM generates bad code?**
A: That's what the cache is for. Check `.wishful/`, tweak it, commit it, and it's locked in.

Expand Down
1 change: 1 addition & 0 deletions coverage.json

Large diffs are not rendered by default.

97 changes: 97 additions & 0 deletions docs/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
# Changelog

All notable changes to wishful will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.2.0] - 2024-11-24

### Added

#### 🎨 Type Registry System
- **Type registration decorator** (`@wishful.type`) for Pydantic models, dataclasses, and TypedDict
- **Pydantic Field constraint support**: LLM now respects `min_length`, `max_length`, `gt`, `ge`, `lt`, `le`, and `pattern` constraints
- **Docstring-driven LLM behavior**: Class docstrings influence generated code tone and style (e.g., "written by master yoda" generates Yoda-speak)
- **Type binding to functions**: `@wishful.type(output_for="function_name")` tells LLM which functions should return which types
- **Multi-function type sharing**: `wishful.type(TypeClass, output_for=["func1", "func2"])` for shared types

#### πŸ”„ Static vs Dynamic Namespaces
- **`wishful.static.*` namespace**: Cached generation (default behavior, fast subsequent imports)
- **`wishful.dynamic.*` namespace**: Runtime-aware regeneration on every import (for creative/contextual content)
- Both namespaces share the same cache file for consistency

#### 🧠 Enhanced Context Discovery
- **Type schema integration**: Registered types are automatically included in LLM prompts
- **Function output type hints**: LLM receives information about expected return types
- Type definitions are serialized with full docstrings and Field constraints

#### πŸ“¦ Pydantic v2 Support
- **Metadata-based constraint extraction**: Properly parses Pydantic v2's constraint storage (`MinLen`, `MaxLen`, `Gt`, etc.)
- **`_PydanticGeneralMetadata` handling**: Extracts `pattern` and other general constraints
- **Backward compatibility**: Still supports Pydantic v1 direct attribute access

### Changed

#### System Prompt Updates
- **External library support**: Changed from "only use Python standard library" to "you may use any Python libraries available in the environment"
- Pydantic, requests, and other common libraries now explicitly allowed in generated code

#### Examples
- Added `07_typed_outputs.py`: Comprehensive type registry demonstration
- Added `08_dynamic_vs_static.py`: Static vs dynamic namespace comparison
- Added `09_context_shenanigans.py`: Context discovery behavior showcase
- All examples updated to use `wishful.static.*` namespace convention

#### Documentation
- **AGENTS.md**: Complete sync with current codebase state
- Added Pydantic Field constraint documentation
- Added docstring influence documentation
- Added type registry implementation details
- Updated TDD process documentation
- **README.md**: Added type registry section, static/dynamic namespace explanation, and updated FAQ

### Internal Improvements

#### Type Registry (`src/wishful/types/`)
- `_build_field_args()`: New method to extract Field() arguments from Pydantic field_info
- `_serialize_pydantic()`: Enhanced to include Field constraints in serialized schemas
- Docstring serialization for all type systems (Pydantic, dataclass, TypedDict)

#### Discovery System (`src/wishful/core/discovery.py`)
- `ImportContext`: Extended with `type_schemas` and `function_output_types` fields
- `discover()`: Now fetches registered type schemas and output type bindings
- Integration with `wishful.types.get_all_type_schemas()` and `get_output_type_for_function()`

#### LLM Prompts (`src/wishful/llm/prompts.py`)
- Enhanced `build_messages()` to include type definitions in prompts
- System prompt updated to allow external libraries
- Type schemas formatted as executable Python code for LLM

### Tests
- **83 total tests** with **80% code coverage**
- Added 4 new tests in `test_discovery.py` for type registry integration
- Added 30 tests in `test_types.py` for type serialization (all scenarios)
- Added 6 tests in `test_namespaces.py` for static vs dynamic behavior

### Dependencies
- Added `pydantic>=2.12.4` as runtime dependency

---

## [0.1.6] - 2024-11-XX

### Initial Release
- Basic import hook system with LLM code generation
- Cache management (static `.wishful/` directory)
- Context discovery from import sites
- Safety validation (AST-based checks)
- CLI interface (`wishful inspect`, `clear`, `regen`)
- Configuration system with environment variables
- litellm integration for multi-provider LLM support
- Fake LLM mode for deterministic testing (`WISHFUL_FAKE_LLM=1`)

---

[0.2.0]: https://github.com/pyros-projects/wishful/compare/v0.1.6...v0.2.0
[0.1.6]: https://github.com/pyros-projects/wishful/releases/tag/v0.1.6
Loading