voodoo-framework 1.0.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.
Files changed (31) hide show
  1. voodoo_framework-1.0.0/PKG-INFO +23 -0
  2. voodoo_framework-1.0.0/README.md +78 -0
  3. voodoo_framework-1.0.0/pyproject.toml +92 -0
  4. voodoo_framework-1.0.0/setup.cfg +4 -0
  5. voodoo_framework-1.0.0/tests/test_components.py +53 -0
  6. voodoo_framework-1.0.0/tests/test_data.py +95 -0
  7. voodoo_framework-1.0.0/tests/test_i18n.py +73 -0
  8. voodoo_framework-1.0.0/tests/test_queue.py +35 -0
  9. voodoo_framework-1.0.0/tests/test_theme.py +33 -0
  10. voodoo_framework-1.0.0/tests/test_websocket.py +52 -0
  11. voodoo_framework-1.0.0/voodoo/__init__.py +51 -0
  12. voodoo_framework-1.0.0/voodoo/agent.py +37 -0
  13. voodoo_framework-1.0.0/voodoo/api.py +143 -0
  14. voodoo_framework-1.0.0/voodoo/cli.py +176 -0
  15. voodoo_framework-1.0.0/voodoo/components.py +107 -0
  16. voodoo_framework-1.0.0/voodoo/config.py +51 -0
  17. voodoo_framework-1.0.0/voodoo/core.py +236 -0
  18. voodoo_framework-1.0.0/voodoo/data.py +179 -0
  19. voodoo_framework-1.0.0/voodoo/i18n.py +107 -0
  20. voodoo_framework-1.0.0/voodoo/mcp.py +141 -0
  21. voodoo_framework-1.0.0/voodoo/queue.py +61 -0
  22. voodoo_framework-1.0.0/voodoo/status.py +131 -0
  23. voodoo_framework-1.0.0/voodoo/storage.py +85 -0
  24. voodoo_framework-1.0.0/voodoo/telemetry.py +157 -0
  25. voodoo_framework-1.0.0/voodoo/theme.py +112 -0
  26. voodoo_framework-1.0.0/voodoo_framework.egg-info/PKG-INFO +23 -0
  27. voodoo_framework-1.0.0/voodoo_framework.egg-info/SOURCES.txt +29 -0
  28. voodoo_framework-1.0.0/voodoo_framework.egg-info/dependency_links.txt +1 -0
  29. voodoo_framework-1.0.0/voodoo_framework.egg-info/entry_points.txt +2 -0
  30. voodoo_framework-1.0.0/voodoo_framework.egg-info/requires.txt +19 -0
  31. voodoo_framework-1.0.0/voodoo_framework.egg-info/top_level.txt +1 -0
@@ -0,0 +1,23 @@
1
+ Metadata-Version: 2.4
2
+ Name: voodoo-framework
3
+ Version: 1.0.0
4
+ Summary: Voodoo Framework
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: starlette
7
+ Requires-Dist: uvicorn
8
+ Requires-Dist: pydantic
9
+ Requires-Dist: aiosqlite
10
+ Requires-Dist: websockets
11
+ Requires-Dist: httpx
12
+ Requires-Dist: python-dotenv
13
+ Requires-Dist: pyyaml
14
+ Requires-Dist: typer
15
+ Requires-Dist: rich
16
+ Requires-Dist: openai
17
+ Requires-Dist: aiofiles
18
+ Provides-Extra: dev
19
+ Requires-Dist: pytest; extra == "dev"
20
+ Requires-Dist: pytest-asyncio; extra == "dev"
21
+ Requires-Dist: ruff; extra == "dev"
22
+ Requires-Dist: mypy; extra == "dev"
23
+ Requires-Dist: pre-commit; extra == "dev"
@@ -0,0 +1,78 @@
1
+ # Voodoo Framework 🪄
2
+
3
+ **The Modern, Full-Stack Async Python Web Framework.**
4
+
5
+ Voodoo is an ultra-minimalist, high-performance web framework designed for the AI era. Built on top of Starlette and Uvicorn, it offers a seamless, reactive developer experience by combining a React-like component system in pure Python with robust backend features like background queues, real-time websockets, LLM agents, and built-in SQLite persistence.
6
+
7
+ ## ✨ Features
8
+
9
+ - **Component-Driven UI**: Build modern, reactive UIs using pure Python components (`Div`, `Card`, `Button`, `Text`) with TailwindCSS integration out of the box. No writing raw HTML/JS required.
10
+ - **Real-Time WebSockets**: Live updates pushed seamlessly to the client with minimal configuration.
11
+ - **Data Persistence**: Built-in async SQLite (`aiosqlite`) ORM using Pydantic models with Row-Level Security (RLS) policies.
12
+ - **Background Workers**: Native task queues for heavy processing, ideal for AI/LLM workloads.
13
+ - **AI Native**: Built-in support for LLM Agents and the Model Context Protocol (MCP).
14
+ - **Observability**: Integrated Telemetry and APM tracing module tracking latencies, metrics, and AI token usage.
15
+ - **Zero Config**: `pyproject.toml` based, zero-configuration setup, hot-reloading, and out-of-the-box smart documentation.
16
+
17
+ ## 🚀 Getting Started
18
+
19
+ ### Prerequisites
20
+
21
+ - Python 3.12+
22
+ - `pip`
23
+
24
+ ### Installation
25
+
26
+ Clone the repository and install the framework in editable mode with development dependencies:
27
+
28
+ ```bash
29
+ git clone https://github.com/your-org/voodoo-framework.git
30
+ cd voodoo-framework
31
+ pip install -e .[dev]
32
+ ```
33
+
34
+ ### Running the App
35
+
36
+ Start the development server using the CLI:
37
+
38
+ ```bash
39
+ voodoo dev
40
+ ```
41
+
42
+ The application will start on `http://localhost:8000`.
43
+ Check out the built-in Smart Documentation at `http://localhost:8000/docs` to see all available components and APIs.
44
+
45
+ ## 📖 Smart Documentation
46
+
47
+ Voodoo ships with its own interactive documentation built using the framework itself. When you run the application, navigate to `/docs` in your browser to view:
48
+
49
+ - Component Library References
50
+ - API Integration Guides
51
+ - Framework Telemetry Status
52
+ - Live Examples
53
+
54
+ ## 🛠 Project Structure
55
+
56
+ ```
57
+ voodoo-framework/
58
+ ├── app/ # Your application code
59
+ │ ├── pages/ # File-based routing (index.py, docs.py)
60
+ │ ├── layout.py # Main layout component
61
+ │ ├── models.py # Pydantic data models
62
+ │ └── workers.py # Background task handlers
63
+ ├── voodoo/ # Framework core source
64
+ ├── tests/ # Pytest async test suite
65
+ └── pyproject.toml # Project metadata and dependencies
66
+ ```
67
+
68
+ ## 🧪 Testing
69
+
70
+ Run the test suite using `pytest`:
71
+
72
+ ```bash
73
+ pytest
74
+ ```
75
+
76
+ ## 📜 License
77
+
78
+ MIT License. See `LICENSE` for more information.
@@ -0,0 +1,92 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "voodoo-framework"
7
+ version = "1.0.0"
8
+ description = "Voodoo Framework"
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "starlette",
12
+ "uvicorn",
13
+ "pydantic",
14
+ "aiosqlite",
15
+ "websockets",
16
+ "httpx",
17
+ "python-dotenv",
18
+ "pyyaml",
19
+ "typer",
20
+ "rich",
21
+ "openai",
22
+ "aiofiles"
23
+ ]
24
+
25
+ [project.scripts]
26
+ voodoo = "voodoo.cli:app"
27
+
28
+ [project.optional-dependencies]
29
+ dev = [
30
+ "pytest",
31
+ "pytest-asyncio",
32
+ "ruff",
33
+ "mypy",
34
+ "pre-commit"
35
+ ]
36
+
37
+ [tool.setuptools.packages.find]
38
+ where = ["."]
39
+ include = ["voodoo*", "app*"]
40
+
41
+ [tool.pytest.ini_options]
42
+ asyncio_mode = "auto"
43
+ testpaths = ["tests"]
44
+
45
+ [tool.ruff]
46
+ target-version = "py312"
47
+ line-length = 88
48
+ exclude = [
49
+ ".git",
50
+ ".mypy_cache",
51
+ ".pytest_cache",
52
+ ".ruff_cache",
53
+ ".venv",
54
+ "__pycache__",
55
+ "build",
56
+ "dist",
57
+ "venv",
58
+ ]
59
+
60
+ [tool.ruff.lint]
61
+ select = [
62
+ "E", # pycodestyle errors
63
+ "W", # pycodestyle warnings
64
+ "F", # pyflakes
65
+ "I", # isort
66
+ "C", # flake8-comprehensions
67
+ "B", # flake8-bugbear
68
+ "UP", # pyupgrade
69
+ ]
70
+ ignore = []
71
+
72
+ [tool.ruff.format]
73
+ quote-style = "double"
74
+ indent-style = "space"
75
+ skip-magic-trailing-comma = false
76
+ line-ending = "auto"
77
+
78
+ [tool.mypy]
79
+ python_version = "3.12"
80
+ warn_return_any = true
81
+ warn_unused_configs = true
82
+ disallow_untyped_defs = true
83
+ disallow_incomplete_defs = true
84
+ check_untyped_defs = true
85
+ disallow_untyped_decorators = false
86
+ no_implicit_optional = true
87
+ strict_optional = true
88
+
89
+ [[tool.mypy.overrides]]
90
+ module = "tests.*"
91
+ disallow_untyped_defs = false
92
+ disallow_incomplete_defs = false
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,53 @@
1
+ import pytest
2
+ from voodoo.components import Component, Div, Button, Input, Card, Text, Heading, ChatBox, Table
3
+
4
+ def test_component_base():
5
+ comp = Component("Hello", id="test-1", className="bg-red-500", data_custom="value")
6
+ html = comp.render()
7
+ assert html == '<div id="test-1" class="bg-red-500" data-custom="value">Hello</div>'
8
+
9
+ def test_div():
10
+ div = Div("Content", id="d1")
11
+ assert div.render() == '<div id="d1">Content</div>'
12
+
13
+ def test_button():
14
+ btn = Button("Click Me", id="btn-1", on_click="my_action")
15
+ html = btn.render()
16
+ assert html == '<button id="btn-1" onclick="voodoo.sendEvent(\'my_action\', this.id, this.value)">Click Me</button>'
17
+
18
+ def test_input():
19
+ inp = Input(id="inp-1", on_change="input_changed", type="text")
20
+ html = inp.render()
21
+ assert html == '<input id="inp-1" type="text" onchange="voodoo.sendEvent(\'input_changed\', this.id, this.value)" />'
22
+
23
+ def test_card():
24
+ card = Card("Card Content", id="card-1", className="extra-class")
25
+ html = card.render()
26
+ assert html == '<div id="card-1" class="bg-[var(--color-surface)] border border-[var(--color-border)] rounded-xl p-6 shadow-xl extra-class">Card Content</div>'
27
+
28
+ def test_text():
29
+ text = Text("Span Text", id="txt-1")
30
+ assert text.render() == '<span id="txt-1">Span Text</span>'
31
+
32
+ def test_heading():
33
+ h1 = Heading("H1 Title", id="h-1")
34
+ assert h1.render() == '<h1 id="h-1">H1 Title</h1>'
35
+
36
+ h3 = Heading("H3 Title", id="h-3", level=3)
37
+ assert h3.render() == '<h3 id="h-3">H3 Title</h3>'
38
+
39
+ def test_chatbox():
40
+ box = ChatBox("Messages", id="chat-1")
41
+ assert box.render() == '<div id="chat-1" class="flex flex-col space-y-2 overflow-y-auto">Messages</div>'
42
+
43
+ def test_table():
44
+ table = Table(
45
+ headers=["Name", "Age"],
46
+ rows=[["Alice", 30], ["Bob", 25]],
47
+ id="tbl-1",
48
+ className="my-table"
49
+ )
50
+ html = table.render()
51
+ assert html.startswith('<table id="tbl-1" class="my-table">')
52
+ assert '<th class="px-6 py-4 text-left text-xs font-medium text-[var(--color-text-muted)] uppercase tracking-wider">Name</th>' in html
53
+ assert '<td class="px-6 py-4 whitespace-nowrap text-sm text-[var(--color-text)]">Alice</td>' in html
@@ -0,0 +1,95 @@
1
+ import pytest
2
+ import pytest_asyncio
3
+ import asyncio
4
+ from voodoo.data import BaseModel, on_insert, on_update, rls_policy
5
+
6
+ class User(BaseModel):
7
+ name: str
8
+ age: int
9
+ is_active: bool
10
+
11
+ class Post(BaseModel):
12
+ title: str
13
+ content: str
14
+ user_id: int
15
+
16
+ inserted_users = []
17
+ updated_users = []
18
+
19
+ @on_insert(User)
20
+ async def hook_insert_user(user):
21
+ inserted_users.append(user.name)
22
+
23
+ @on_update(User)
24
+ def hook_update_user(user):
25
+ updated_users.append(user.name)
26
+
27
+ @rls_policy(Post)
28
+ def post_policy(context):
29
+ if context.get("role") == "admin":
30
+ return None
31
+ return f"user_id = {context.get('user_id', 0)}"
32
+
33
+ @pytest_asyncio.fixture(autouse=True)
34
+ async def clear_db(test_db):
35
+ await test_db.execute("DELETE FROM user")
36
+ await test_db.execute("DELETE FROM post")
37
+ await test_db.commit()
38
+
39
+ @pytest.mark.asyncio
40
+ async def test_orm_operations(test_db):
41
+ # Insert
42
+ u = User()
43
+ u.name = "Alice"
44
+ u.age = 30
45
+ u.is_active = True
46
+ await u.insert()
47
+
48
+ assert u.id is not None
49
+
50
+ # Wait for the async trigger to execute
51
+ await asyncio.sleep(0.01)
52
+ assert "Alice" in inserted_users
53
+
54
+ # Find all
55
+ users = await User.find_all()
56
+ assert len(users) == 1
57
+ assert users[0].name == "Alice"
58
+ assert users[0].age == 30
59
+ assert users[0].is_active is True
60
+
61
+ # Update
62
+ u.age = 31
63
+ await u.update()
64
+
65
+ # Wait for the sync trigger to execute (called directly in update)
66
+ await asyncio.sleep(0.01)
67
+ assert "Alice" in updated_users
68
+
69
+ users = await User.find_all()
70
+ assert users[0].age == 31
71
+
72
+ @pytest.mark.asyncio
73
+ async def test_rls_policies(test_db):
74
+ p1 = Post()
75
+ p1.title = "Hello"
76
+ p1.content = "World"
77
+ p1.user_id = 1
78
+ await p1.insert()
79
+
80
+ p2 = Post()
81
+ p2.title = "Secret"
82
+ p2.content = "Admin only"
83
+ p2.user_id = 2
84
+ await p2.insert()
85
+
86
+ # User 1 context
87
+ user_context = {"user_id": 1, "role": "user"}
88
+ posts = await Post.find_all(user_context=user_context)
89
+ assert len(posts) == 1
90
+ assert posts[0].title == "Hello"
91
+
92
+ # Admin context
93
+ admin_context = {"user_id": 99, "role": "admin"}
94
+ all_posts = await Post.find_all(user_context=admin_context)
95
+ assert len(all_posts) == 2
@@ -0,0 +1,73 @@
1
+ import os
2
+ import json
3
+ import pytest
4
+ from starlette.testclient import TestClient
5
+ from starlette.applications import Starlette
6
+ from starlette.routing import Route
7
+ from starlette.responses import PlainTextResponse
8
+
9
+ from voodoo.i18n import I18n, _, current_language, I18nMiddleware
10
+
11
+ @pytest.fixture
12
+ def mock_locales(tmp_path):
13
+ locales_dir = tmp_path / "locales"
14
+ locales_dir.mkdir()
15
+
16
+ en_data = {"hello": "Hello", "nested": {"key": "Value {name}"}}
17
+ pt_data = {"hello": "Olá", "nested": {"key": "Valor {name}"}}
18
+
19
+ (locales_dir / "en.json").write_text(json.dumps(en_data), encoding="utf-8")
20
+ (locales_dir / "pt.json").write_text(json.dumps(pt_data), encoding="utf-8")
21
+
22
+ return str(locales_dir)
23
+
24
+ def test_i18n_loading(mock_locales):
25
+ i18n = I18n(locales_dir=mock_locales)
26
+
27
+ assert i18n.get_translation("en", "hello") == "Hello"
28
+ assert i18n.get_translation("pt", "hello") == "Olá"
29
+
30
+ # Fallback to default
31
+ assert i18n.get_translation("es", "hello") == "Hello"
32
+
33
+ # Missing key
34
+ assert i18n.get_translation("en", "missing.key") == "missing.key"
35
+
36
+ # Interpolation
37
+ assert i18n.get_translation("en", "nested.key", name="World") == "Value World"
38
+ assert i18n.get_translation("pt", "nested.key", name="Mundo") == "Valor Mundo"
39
+
40
+ def test_i18n_middleware(mock_locales):
41
+ i18n = I18n(locales_dir=mock_locales)
42
+
43
+ async def homepage(request):
44
+ # We temporarily override the global i18n_instance in the test
45
+ # but here we can just use the context var directly
46
+ import voodoo.i18n
47
+ voodoo.i18n.i18n_instance = i18n
48
+
49
+ return PlainTextResponse(f"Lang: {current_language.get()} - Msg: {_('hello')}")
50
+
51
+ app = Starlette(routes=[Route("/", homepage)])
52
+ app.add_middleware(I18nMiddleware, i18n=i18n)
53
+
54
+ client = TestClient(app)
55
+
56
+ # 1. Default
57
+ resp = client.get("/")
58
+ assert resp.text == "Lang: en - Msg: Hello"
59
+
60
+ # 2. Accept-Language header
61
+ resp = client.get("/", headers={"Accept-Language": "pt-BR,pt;q=0.9,en-US;q=0.8"})
62
+ assert resp.text == "Lang: pt - Msg: Olá"
63
+
64
+ # 3. Cookie
65
+ client.cookies = {"voodoo_lang": "pt"}
66
+ resp = client.get("/")
67
+ assert resp.text == "Lang: pt - Msg: Olá"
68
+
69
+ # 4. Query Param
70
+ resp = client.get("/?lang=en")
71
+ assert resp.text == "Lang: en - Msg: Hello"
72
+ # Query param sets the cookie
73
+ assert "voodoo_lang=en" in resp.headers.get("set-cookie", "")
@@ -0,0 +1,35 @@
1
+ import pytest
2
+ import asyncio
3
+ from voodoo.queue import queue, enqueue, start_workers, stop_workers
4
+
5
+ processed_items = []
6
+
7
+ @queue("test_queue")
8
+ async def process_item(payload):
9
+ processed_items.append(payload)
10
+
11
+ @queue("sync_queue")
12
+ def process_sync_item(payload):
13
+ processed_items.append(f"sync-{payload}")
14
+
15
+ @pytest.mark.asyncio
16
+ async def test_queue_workers():
17
+ processed_items.clear()
18
+
19
+ # Enqueue items
20
+ await enqueue("test_queue", "item1")
21
+ await enqueue("test_queue", "item2")
22
+ await enqueue("sync_queue", "item3")
23
+
24
+ # Start workers
25
+ await start_workers()
26
+
27
+ # Allow workers to process the queue
28
+ await asyncio.sleep(0.1)
29
+
30
+ # Stop workers
31
+ await stop_workers()
32
+
33
+ assert "item1" in processed_items
34
+ assert "item2" in processed_items
35
+ assert "sync-item3" in processed_items
@@ -0,0 +1,33 @@
1
+ import pytest
2
+ from voodoo.theme import Theme, ThemeColors, set_theme, default_theme
3
+
4
+ def test_default_theme():
5
+ assert default_theme.mode == "dark"
6
+ assert default_theme.colors.primary == "#007AFF"
7
+
8
+ def test_custom_theme():
9
+ custom_colors = ThemeColors(primary="#FF0000", extra={"custom": "#00FF00"})
10
+ theme = Theme(mode="light", colors=custom_colors)
11
+
12
+ assert theme.mode == "light"
13
+ assert theme.colors.primary == "#FF0000"
14
+
15
+ css_vars = theme.to_css_variables()
16
+ assert "--color-primary: #FF0000;" in css_vars
17
+ assert "--color-custom: #00FF00;" in css_vars
18
+
19
+ tw_config = theme.to_tailwind_config()
20
+ assert '"primary": "#FF0000"' in tw_config
21
+ assert '"custom": "#00FF00"' in tw_config
22
+
23
+ def test_set_theme():
24
+ original_theme = default_theme
25
+ try:
26
+ new_theme = Theme(mode="light")
27
+ set_theme(new_theme)
28
+
29
+ # In voodoo.theme namespace, default_theme should be updated
30
+ import voodoo.theme
31
+ assert voodoo.theme.default_theme.mode == "light"
32
+ finally:
33
+ set_theme(original_theme)
@@ -0,0 +1,52 @@
1
+ import pytest
2
+ import json
3
+ from voodoo.core import register_event, ws_manager
4
+
5
+ def test_websocket_event(client):
6
+ received = []
7
+
8
+ async def async_dummy_handler(element_id, value):
9
+ received.append((element_id, value))
10
+ await ws_manager.broadcast_patch("target-1", "<b>Updated</b>")
11
+
12
+ register_event("test_action", async_dummy_handler)
13
+
14
+ with client.websocket_connect("/_voodoo_ws") as websocket:
15
+ websocket.send_text(json.dumps({
16
+ "type": "event",
17
+ "event": "test_action",
18
+ "id": "btn-1",
19
+ "value": "clicked"
20
+ }))
21
+
22
+ # Receive the patch response
23
+ response = websocket.receive_text()
24
+ data = json.loads(response)
25
+
26
+ assert data["type"] == "patch"
27
+ assert data["id"] == "target-1"
28
+ assert data["html"] == "<b>Updated</b>"
29
+
30
+ assert len(received) == 1
31
+ assert received[0] == ("btn-1", "clicked")
32
+
33
+ def test_websocket_append(client):
34
+ async def append_handler(element_id, value):
35
+ await ws_manager.broadcast_append("list-1", "<li>New Item</li>")
36
+
37
+ register_event("append_action", append_handler)
38
+
39
+ with client.websocket_connect("/_voodoo_ws") as websocket:
40
+ websocket.send_text(json.dumps({
41
+ "type": "event",
42
+ "event": "append_action",
43
+ "id": "add-btn",
44
+ "value": ""
45
+ }))
46
+
47
+ response = websocket.receive_text()
48
+ data = json.loads(response)
49
+
50
+ assert data["type"] == "append"
51
+ assert data["id"] == "list-1"
52
+ assert data["html"] == "<li>New Item</li>"
@@ -0,0 +1,51 @@
1
+ from .core import create_app, register_event, ws_manager
2
+ from .components import Div, Button, Input, Card, Text, Heading, ChatBox, Table
3
+ from .data import BaseModel, on_insert, on_update, rls_policy, get_db
4
+ from .queue import queue, enqueue
5
+ from .agent import Agent
6
+ from .api import api
7
+ from .storage import storage
8
+ from .mcp import mcp, MCPClient
9
+ from .status import ServiceStatus
10
+ from .telemetry import trace, TelemetryMiddleware, telemetry_store
11
+ from .config import config
12
+ from .theme import Theme, ThemeColors, set_theme, default_theme
13
+ from .i18n import _, I18n, i18n_instance
14
+
15
+ __all__ = [
16
+ "create_app",
17
+ "register_event",
18
+ "ws_manager",
19
+ "Div",
20
+ "Button",
21
+ "Input",
22
+ "Card",
23
+ "Text",
24
+ "Heading",
25
+ "ChatBox",
26
+ "Table",
27
+ "BaseModel",
28
+ "on_insert",
29
+ "on_update",
30
+ "rls_policy",
31
+ "get_db",
32
+ "queue",
33
+ "enqueue",
34
+ "Agent",
35
+ "api",
36
+ "storage",
37
+ "mcp",
38
+ "MCPClient",
39
+ "ServiceStatus",
40
+ "trace",
41
+ "TelemetryMiddleware",
42
+ "telemetry_store",
43
+ "config",
44
+ "Theme",
45
+ "ThemeColors",
46
+ "set_theme",
47
+ "default_theme",
48
+ "_",
49
+ "I18n",
50
+ "i18n_instance"
51
+ ]
@@ -0,0 +1,37 @@
1
+ import asyncio
2
+ from typing import AsyncGenerator, List, Dict
3
+
4
+ class Agent:
5
+ def __init__(self, system_prompt: str = "You are a helpful AI assistant."):
6
+ self.system_prompt = system_prompt
7
+ self.history: List[Dict[str, str]] = [
8
+ {"role": "system", "content": system_prompt}
9
+ ]
10
+
11
+ async def stream(self, prompt: str) -> AsyncGenerator[str, None]:
12
+ from voodoo.telemetry import telemetry_store
13
+ self.history.append({"role": "user", "content": prompt})
14
+
15
+ # In a real scenario, this would connect to OpenAI/Anthropic etc.
16
+ # For this standalone framework, we'll simulate a streaming response
17
+ # or use an actual API if configured.
18
+
19
+ # Simulating a stream of tokens:
20
+ words = f"This is an AI response to: '{prompt}'. In a real app, this streams from an LLM. Here are some more tokens to simulate streaming.".split(" ")
21
+
22
+ telemetry_store.record_agent_tokens(len(prompt.split()) + len(words))
23
+
24
+ full_response = ""
25
+ for word in words:
26
+ await asyncio.sleep(0.1) # Simulate network latency
27
+ token = word + " "
28
+ full_response += token
29
+ yield token
30
+
31
+ self.history.append({"role": "assistant", "content": full_response.strip()})
32
+
33
+ async def run(self, prompt: str) -> str:
34
+ response = ""
35
+ async for chunk in self.stream(prompt):
36
+ response += chunk
37
+ return response