stapel-agent 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.
Files changed (42) hide show
  1. stapel_agent/__init__.py +55 -0
  2. stapel_agent/admin.py +35 -0
  3. stapel_agent/apps.py +17 -0
  4. stapel_agent/cache.py +85 -0
  5. stapel_agent/checks.py +77 -0
  6. stapel_agent/conf.py +70 -0
  7. stapel_agent/conftest.py +123 -0
  8. stapel_agent/dto.py +55 -0
  9. stapel_agent/errors.py +21 -0
  10. stapel_agent/functions.py +93 -0
  11. stapel_agent/migrations/0001_initial.py +43 -0
  12. stapel_agent/migrations/__init__.py +0 -0
  13. stapel_agent/models.py +67 -0
  14. stapel_agent/parsing.py +86 -0
  15. stapel_agent/providers/__init__.py +95 -0
  16. stapel_agent/providers/anthropic.py +63 -0
  17. stapel_agent/providers/base.py +61 -0
  18. stapel_agent/providers/claude_cli.py +79 -0
  19. stapel_agent/providers/openai_compat.py +81 -0
  20. stapel_agent/py.typed +0 -0
  21. stapel_agent/schemas/functions/llm.complete.json +27 -0
  22. stapel_agent/schemas/functions/llm.translate.json +23 -0
  23. stapel_agent/serializers.py +37 -0
  24. stapel_agent/services.py +282 -0
  25. stapel_agent/tests/__init__.py +0 -0
  26. stapel_agent/tests/fakes.py +84 -0
  27. stapel_agent/tests/test_api.py +201 -0
  28. stapel_agent/tests/test_extension_points.py +210 -0
  29. stapel_agent/tests/test_functions.py +79 -0
  30. stapel_agent/tests/test_models_and_admin.py +93 -0
  31. stapel_agent/tests/test_parsing.py +83 -0
  32. stapel_agent/tests/test_providers.py +334 -0
  33. stapel_agent/tests/test_public_api.py +100 -0
  34. stapel_agent/tests/test_services.py +203 -0
  35. stapel_agent/tests/urls.py +5 -0
  36. stapel_agent/urls.py +19 -0
  37. stapel_agent/views.py +114 -0
  38. stapel_agent-0.1.0.dist-info/METADATA +155 -0
  39. stapel_agent-0.1.0.dist-info/RECORD +42 -0
  40. stapel_agent-0.1.0.dist-info/WHEEL +5 -0
  41. stapel_agent-0.1.0.dist-info/licenses/LICENSE +21 -0
  42. stapel_agent-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,55 @@
1
+ """stapel-agent — LLM facade: completion, translation, prompt cache/ledger.
2
+
3
+ Public API (lazily resolved, PEP 562 — importing this package pulls in
4
+ no Django code until an attribute is actually accessed):
5
+
6
+ agent_settings — the ``STAPEL_AGENT`` settings namespace
7
+ complete — raw LLM completion (cache + PromptLog ledger)
8
+ translate — key-value translation flow
9
+ LlmProvider — base class for custom LLM backends
10
+ ProviderResult — completion text + token accounting dataclass
11
+ CachePolicy — base class for custom prompt-cache policies
12
+ register_provider — runtime provider registration (apps.ready())
13
+ registered_providers — the effective provider registry mapping
14
+ """
15
+
16
+ __all__ = [
17
+ "CachePolicy",
18
+ "LlmProvider",
19
+ "ProviderResult",
20
+ "agent_settings",
21
+ "complete",
22
+ "register_provider",
23
+ "registered_providers",
24
+ "translate",
25
+ ]
26
+
27
+ # name -> (relative module, attribute)
28
+ _EXPORTS = {
29
+ "agent_settings": (".conf", "agent_settings"),
30
+ "complete": (".services", "complete"),
31
+ "translate": (".services", "translate"),
32
+ "LlmProvider": (".providers.base", "LlmProvider"),
33
+ "ProviderResult": (".providers.base", "ProviderResult"),
34
+ "CachePolicy": (".cache", "CachePolicy"),
35
+ "register_provider": (".providers", "register_provider"),
36
+ "registered_providers": (".providers", "registered_providers"),
37
+ }
38
+
39
+
40
+ def __getattr__(name):
41
+ try:
42
+ module_path, attr = _EXPORTS[name]
43
+ except KeyError:
44
+ raise AttributeError(
45
+ f"module {__name__!r} has no attribute {name!r}"
46
+ ) from None
47
+ from importlib import import_module
48
+
49
+ value = getattr(import_module(module_path, __name__), attr)
50
+ globals()[name] = value # cache: subsequent lookups skip __getattr__
51
+ return value
52
+
53
+
54
+ def __dir__():
55
+ return sorted(set(globals()) | set(__all__))
stapel_agent/admin.py ADDED
@@ -0,0 +1,35 @@
1
+ from django.contrib import admin
2
+
3
+ from .models import PromptLog
4
+
5
+
6
+ @admin.register(PromptLog)
7
+ class PromptLogAdmin(admin.ModelAdmin):
8
+ """Read-only: PromptLog is an immutable ledger — rows are written by
9
+ the service layer only (editing one would corrupt token accounting
10
+ and could poison the prompt cache)."""
11
+
12
+ list_display = [
13
+ "created_at",
14
+ "source",
15
+ "model",
16
+ "model_size",
17
+ "status",
18
+ "input_tokens",
19
+ "output_tokens",
20
+ "duration_ms",
21
+ "user_id",
22
+ ]
23
+ list_filter = ["source", "status", "model_size"]
24
+ search_fields = ["prompt", "user_id", "model"]
25
+ date_hierarchy = "created_at"
26
+ ordering = ["-created_at"]
27
+
28
+ def has_add_permission(self, request):
29
+ return False
30
+
31
+ def has_change_permission(self, request, obj=None):
32
+ return False
33
+
34
+ def has_delete_permission(self, request, obj=None):
35
+ return False
stapel_agent/apps.py ADDED
@@ -0,0 +1,17 @@
1
+ from django.apps import AppConfig
2
+
3
+
4
+ class AgentConfig(AppConfig):
5
+ default_auto_field = "django.db.models.BigAutoField"
6
+ name = "stapel_agent"
7
+ label = "agent"
8
+ verbose_name = "Stapel Agent"
9
+
10
+ def ready(self):
11
+ # comm Function providers (in-process in a monolith, transport
12
+ # chosen by STAPEL_COMM in microservices — same code).
13
+ from . import functions # noqa: F401
14
+
15
+ # Django system checks (provider registry / DEFAULT_PROVIDER
16
+ # misconfiguration) — registered on import.
17
+ from . import checks # noqa: F401
stapel_agent/cache.py ADDED
@@ -0,0 +1,85 @@
1
+ """Cache-policy seam — swap the prompt cache without forking.
2
+
3
+ ``STAPEL_AGENT["CACHE_POLICY"]`` is a dotted path to a ``CachePolicy``
4
+ subclass (resolved via ``import_strings``, instantiated per call). The
5
+ default, ``PromptLogCachePolicy``, implements the stock behaviour: the
6
+ latest successful ``PromptLog`` row with an identical
7
+ prompt+system_prompt+source within ``CACHE_TTL``, gated per source by
8
+ ``CACHE_LOOKUP``.
9
+
10
+ Hosts can point the setting at a Redis-backed policy, a no-op policy,
11
+ or anything else::
12
+
13
+ # myproject/llm_cache.py
14
+ from stapel_agent.cache import CachePolicy
15
+
16
+ class RedisCachePolicy(CachePolicy):
17
+ def should_cache(self, source): ...
18
+ def lookup(self, prompt, system_prompt, source): ...
19
+ def store(self, prompt, system_prompt, source, response): ...
20
+
21
+ # settings.py
22
+ STAPEL_AGENT = {"CACHE_POLICY": "myproject.llm_cache.RedisCachePolicy"}
23
+
24
+ The PromptLog *ledger* row is always written regardless of the policy —
25
+ caching is a read seam, accounting is not optional. ``store()`` exists
26
+ for policies with their own storage; the default is a no-op because the
27
+ ledger row IS the default policy's storage.
28
+ """
29
+ from __future__ import annotations
30
+
31
+ from abc import ABC, abstractmethod
32
+ from datetime import timedelta
33
+
34
+
35
+ class CachePolicy(ABC):
36
+ """Decides when to consult the prompt cache and answers lookups."""
37
+
38
+ @abstractmethod
39
+ def should_cache(self, source: str) -> bool:
40
+ """Whether *source* ("llm_facade"/"translate"/...) uses the cache."""
41
+
42
+ @abstractmethod
43
+ def lookup(self, prompt: str, system_prompt: str | None, source: str) -> str | None:
44
+ """Return the cached raw response text, or None on a miss."""
45
+
46
+ def store(self, prompt: str, system_prompt: str | None, source: str, response: str) -> None:
47
+ """Persist a successful response for future lookups.
48
+
49
+ No-op by default: the default policy reads the PromptLog ledger
50
+ row that ``services.complete`` writes anyway. Policies with
51
+ external storage (Redis, ...) override this.
52
+ """
53
+
54
+
55
+ class PromptLogCachePolicy(CachePolicy):
56
+ """Stock policy: PromptLog rows + CACHE_LOOKUP/CACHE_TTL settings."""
57
+
58
+ def should_cache(self, source: str) -> bool:
59
+ from .conf import agent_settings
60
+
61
+ return bool((agent_settings.CACHE_LOOKUP or {}).get(source, False))
62
+
63
+ def lookup(self, prompt: str, system_prompt: str | None, source: str) -> str | None:
64
+ from django.utils import timezone
65
+
66
+ from .conf import agent_settings
67
+ from .models import PromptLog, PromptStatus
68
+
69
+ ttl = int(agent_settings.CACHE_TTL)
70
+ qs = PromptLog.objects.filter(
71
+ prompt=prompt,
72
+ source=source,
73
+ status=PromptStatus.SUCCESS,
74
+ response__isnull=False,
75
+ created_at__gte=timezone.now() - timedelta(seconds=ttl),
76
+ )
77
+ if system_prompt:
78
+ qs = qs.filter(system_prompt=system_prompt)
79
+ else:
80
+ qs = qs.filter(system_prompt__isnull=True)
81
+ row = qs.order_by("-created_at").first()
82
+ return row.response if row is not None else None
83
+
84
+
85
+ __all__ = ["CachePolicy", "PromptLogCachePolicy"]
stapel_agent/checks.py ADDED
@@ -0,0 +1,77 @@
1
+ """Django system checks — catch provider misconfiguration at startup.
2
+
3
+ Registered from ``AgentConfig.ready()``. IDs:
4
+
5
+ - ``stapel_agent.E001`` — ``DEFAULT_PROVIDER`` names a provider that is
6
+ not in the effective registry (built-ins ← settings merge ← runtime).
7
+ - ``stapel_agent.W001`` — a registry entry's dotted path fails to import
8
+ (typo, or an optional dependency missing in this image).
9
+ - ``stapel_agent.W002`` — a registry entry resolves to something that is
10
+ not an ``LlmProvider`` subclass.
11
+
12
+ Import/subclass problems are warnings, not errors, on purpose: providers
13
+ resolve lazily per request and degrade to ``status: "failure"`` — a
14
+ broken *unused* entry must not block deploys, but it should be visible.
15
+ """
16
+ from __future__ import annotations
17
+
18
+ import inspect
19
+
20
+ from django.core import checks
21
+ from django.utils.module_loading import import_string
22
+
23
+
24
+ @checks.register("stapel_agent")
25
+ def check_providers(app_configs, **kwargs):
26
+ from .conf import agent_settings
27
+ from .providers import registered_providers
28
+ from .providers.base import LlmProvider
29
+
30
+ issues = []
31
+ effective = registered_providers()
32
+
33
+ default = agent_settings.DEFAULT_PROVIDER
34
+ if default not in effective:
35
+ issues.append(
36
+ checks.Error(
37
+ f"STAPEL_AGENT['DEFAULT_PROVIDER'] is {default!r}, which is "
38
+ "not in the effective provider registry "
39
+ f"({sorted(effective) or 'empty'}).",
40
+ hint=(
41
+ "Add it via STAPEL_AGENT['PROVIDERS'] or "
42
+ "stapel_agent.providers.register_provider(), or point "
43
+ "DEFAULT_PROVIDER at an existing name."
44
+ ),
45
+ id="stapel_agent.E001",
46
+ )
47
+ )
48
+
49
+ for name, target in effective.items():
50
+ if isinstance(target, str):
51
+ try:
52
+ target = import_string(target)
53
+ except ImportError as exc:
54
+ issues.append(
55
+ checks.Warning(
56
+ f"LLM provider {name!r} cannot be imported: {exc}",
57
+ hint=(
58
+ "Fix the dotted path, install the missing "
59
+ "dependency, or remove the entry (set it to None)."
60
+ ),
61
+ id="stapel_agent.W001",
62
+ )
63
+ )
64
+ continue
65
+ if not (inspect.isclass(target) and issubclass(target, LlmProvider)):
66
+ issues.append(
67
+ checks.Warning(
68
+ f"LLM provider {name!r} resolves to {target!r}, which is "
69
+ "not a stapel_agent.LlmProvider subclass.",
70
+ hint="Implement the LlmProvider ABC (see MODULE.md).",
71
+ id="stapel_agent.W002",
72
+ )
73
+ )
74
+ return issues
75
+
76
+
77
+ __all__ = ["check_providers"]
stapel_agent/conf.py ADDED
@@ -0,0 +1,70 @@
1
+ """Settings namespace for stapel-agent.
2
+
3
+ All configuration is read through ``agent_settings`` (lazily, at call
4
+ time) instead of module-level ``os.getenv`` — so tests and host projects
5
+ can override any key via ``settings.STAPEL_AGENT``, a flat Django setting
6
+ of the same name, or an environment variable::
7
+
8
+ STAPEL_AGENT = {
9
+ "DEFAULT_PROVIDER": "openai-compat",
10
+ "OPENAI_COMPAT_BASE_URL": "https://api.deepseek.com/v1",
11
+ "OPENAI_COMPAT_API_KEY": "sk-...",
12
+ "OPENAI_COMPAT_MODELS": {"small": "deepseek-chat"},
13
+ }
14
+
15
+ ``PROVIDERS`` entries are **merged over** the built-in registry
16
+ (``stapel_agent.providers.BUILTIN_PROVIDERS``) — adding one custom
17
+ provider does not require restating the built-ins, and setting a name to
18
+ ``None``/``""`` removes it. Values are dotted paths to ``LlmProvider``
19
+ subclasses, resolved lazily per request in ``services.get_provider``
20
+ (not via ``import_strings`` — an unknown or broken provider must degrade
21
+ to a ``status: failure`` response, never an import-time crash).
22
+ """
23
+ from stapel_core.conf import AppSettings
24
+
25
+ agent_settings = AppSettings(
26
+ "STAPEL_AGENT",
27
+ defaults={
28
+ # Size → model-name map used by the default (Anthropic-flavoured)
29
+ # providers. OpenAI-compatible hosts override per-size names via
30
+ # OPENAI_COMPAT_MODELS instead.
31
+ "MODELS": {
32
+ "small": "claude-haiku-4-5-20251001",
33
+ "medium": "claude-sonnet-5",
34
+ "large": "claude-opus-4-8",
35
+ },
36
+ # Overlay merged OVER providers.BUILTIN_PROVIDERS (anthropic /
37
+ # openai-compat / claude-code): add or override entries here,
38
+ # None/"" removes a name. Resolved lazily per request via
39
+ # import_string in services.get_provider(name).
40
+ "PROVIDERS": {},
41
+ "DEFAULT_PROVIDER": "anthropic",
42
+ # Anthropic SDK (read lazily at call time, never frozen at import).
43
+ "ANTHROPIC_API_KEY": "",
44
+ # Any OpenAI-compatible /chat/completions endpoint
45
+ # (OpenAI, DeepSeek, MiMo, GLM, Kimi, ...).
46
+ "OPENAI_COMPAT_BASE_URL": "",
47
+ "OPENAI_COMPAT_API_KEY": "",
48
+ # Optional size → model-name map for the openai-compat provider,
49
+ # e.g. {"small": "gpt-4o-mini", "medium": "gpt-4o"}. Missing sizes
50
+ # fall back to MODELS[size].
51
+ "OPENAI_COMPAT_MODELS": {},
52
+ # Claude Code CLI provider (opt-in only, never the default).
53
+ "CLI_BINARY": "claude",
54
+ "CLI_TIMEOUT": 120,
55
+ "MAX_TOKENS": 4096,
56
+ # Per-source cache-by-prompt toggle: a repeated identical
57
+ # prompt+system_prompt within CACHE_TTL returns the stored response
58
+ # without calling the provider.
59
+ "CACHE_LOOKUP": {"llm_facade": False, "translate": True},
60
+ # Seconds; cached rows older than this are ignored (7 days).
61
+ "CACHE_TTL": 604800,
62
+ # Dotted path to a stapel_agent.cache.CachePolicy subclass — the
63
+ # cache seam. The default implements the PromptLog+TTL behaviour;
64
+ # swap for Redis/no-op without forking.
65
+ "CACHE_POLICY": "stapel_agent.cache.PromptLogCachePolicy",
66
+ },
67
+ import_strings=("CACHE_POLICY",),
68
+ )
69
+
70
+ __all__ = ["agent_settings"]
@@ -0,0 +1,123 @@
1
+ def pytest_configure(config):
2
+ from django.conf import settings
3
+ if not settings.configured:
4
+ settings.configure(
5
+ SECRET_KEY="test-secret-key-not-for-production",
6
+ INSTALLED_APPS=[
7
+ "django.contrib.contenttypes",
8
+ "django.contrib.auth",
9
+ "django.contrib.sessions",
10
+ "django.contrib.messages",
11
+ # contrib.admin so the ModelAdmin registrations in admin.py
12
+ # are importable (and covered) in tests.
13
+ "django.contrib.admin",
14
+ "stapel_core.django.users",
15
+ "rest_framework",
16
+ "stapel_agent",
17
+ ],
18
+ AUTH_USER_MODEL="users.User",
19
+ DATABASES={
20
+ "default": {
21
+ "ENGINE": "django.db.backends.sqlite3",
22
+ "NAME": ":memory:",
23
+ }
24
+ },
25
+ DEFAULT_AUTO_FIELD="django.db.models.BigAutoField",
26
+ USE_TZ=True,
27
+ ROOT_URLCONF="stapel_agent.tests.urls",
28
+ CACHES={
29
+ "default": {
30
+ "BACKEND": "django.core.cache.backends.locmem.LocMemCache",
31
+ }
32
+ },
33
+ # In-memory bus — no Kafka/Redis broker needed
34
+ STAPEL_BUS_BACKEND="stapel_core.bus.backends.memory.MemoryBus",
35
+ # Synchronous in-process comm and schema validation ON so
36
+ # llm.complete / llm.translate payloads are checked against
37
+ # schemas/functions/*.json in tests.
38
+ STAPEL_COMM={
39
+ "OUTBOX_ENABLED": False,
40
+ "ACTION_TRANSPORT": "inprocess",
41
+ "FUNCTION_TRANSPORT": "inprocess",
42
+ "VALIDATE_SCHEMAS": True,
43
+ },
44
+ MIDDLEWARE=[
45
+ "django.middleware.common.CommonMiddleware",
46
+ "stapel_core.django.jwt.middleware.ServiceAPIKeyMiddleware",
47
+ ],
48
+ SERVICE_API_KEY="test-service-key",
49
+ # Skip migrations — create tables directly from models
50
+ MIGRATION_MODULES={
51
+ "users": None,
52
+ "agent": None,
53
+ },
54
+ )
55
+ import django
56
+ django.setup()
57
+
58
+ # Register schemas/functions/*.json with the comm registries so
59
+ # call() payloads are validated against the committed contracts.
60
+ from stapel_core.comm.schemas import autoload_schemas
61
+ autoload_schemas()
62
+
63
+
64
+ import pytest # noqa: E402
65
+
66
+
67
+ @pytest.fixture
68
+ def user(db):
69
+ from django.contrib.auth import get_user_model
70
+ User = get_user_model()
71
+ return User.objects.create_user(
72
+ username="testuser",
73
+ email="testuser@example.com",
74
+ password="testpass123",
75
+ )
76
+
77
+
78
+ @pytest.fixture
79
+ def staff_user(db):
80
+ from django.contrib.auth import get_user_model
81
+ User = get_user_model()
82
+ return User.objects.create_user(
83
+ username="staffuser",
84
+ email="staff@example.com",
85
+ password="testpass123",
86
+ is_staff=True,
87
+ )
88
+
89
+
90
+ @pytest.fixture
91
+ def api_client():
92
+ from rest_framework.test import APIClient
93
+ return APIClient()
94
+
95
+
96
+ @pytest.fixture
97
+ def staff_client(staff_user):
98
+ from rest_framework.test import APIClient
99
+ client = APIClient()
100
+ client.force_authenticate(user=staff_user)
101
+ return client
102
+
103
+
104
+ @pytest.fixture
105
+ def fake_provider(settings):
106
+ """Route completions to the recording FakeProvider (default provider).
107
+
108
+ Keys are read lazily through stapel_agent.conf.agent_settings, so the
109
+ settings override takes effect at call time. Class-level state is
110
+ reset around each test — get_provider() instantiates a fresh object
111
+ per request, so recordings must live on the class.
112
+ """
113
+ from stapel_agent.tests.fakes import FakeProvider
114
+
115
+ # Merge semantics: adding "fake" does not restate the built-ins —
116
+ # they stay resolvable alongside it.
117
+ settings.STAPEL_AGENT = {
118
+ "PROVIDERS": {"fake": "stapel_agent.tests.fakes.FakeProvider"},
119
+ "DEFAULT_PROVIDER": "fake",
120
+ }
121
+ FakeProvider.reset()
122
+ yield FakeProvider
123
+ FakeProvider.reset()
stapel_agent/dto.py ADDED
@@ -0,0 +1,55 @@
1
+ """DTOs for the agent API."""
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Dict, Optional
5
+
6
+
7
+ @dataclass
8
+ class CompleteRequest:
9
+ """JSON LLM completion request.
10
+
11
+ Attributes:
12
+ prompt: The user prompt sent to the model.
13
+ model: Model size — small, medium or large. Example: small
14
+ provider: Provider name from STAPEL_AGENT["PROVIDERS"]; defaults
15
+ to DEFAULT_PROVIDER.
16
+ system_prompt: Replaces the built-in JSON-API system prompt.
17
+ """
18
+
19
+ prompt: str
20
+ model: str
21
+ provider: Optional[str] = None
22
+ system_prompt: Optional[str] = None
23
+
24
+
25
+ @dataclass
26
+ class TranslateRequest:
27
+ """Key-value translation request.
28
+
29
+ The wire key for the source language is ``from`` (a Python keyword) —
30
+ the serializer maps it onto ``from_lang`` explicitly.
31
+
32
+ Attributes:
33
+ from_lang: Source language code, or "auto" to auto-detect.
34
+ to: Target language code. Example: de
35
+ entries: Mapping of keys to source-language strings.
36
+ """
37
+
38
+ from_lang: str
39
+ to: str
40
+ entries: Dict[str, str] = field(default_factory=dict)
41
+
42
+
43
+ @dataclass
44
+ class TranslateResponse:
45
+ """Key-value translation response (iron-agent contract).
46
+
47
+ Attributes:
48
+ status: "ok" or "failure".
49
+ result: Mapping of keys to translated strings (on success).
50
+ reason: Failure reason (on failure).
51
+ """
52
+
53
+ status: str
54
+ result: Optional[Dict[str, str]] = None
55
+ reason: Optional[str] = None
stapel_agent/errors.py ADDED
@@ -0,0 +1,21 @@
1
+ """Custom error keys for the agent service.
2
+
3
+ Only *request validation* problems are error-key responses (HTTP 400).
4
+ LLM/provider failures are NOT errors at the HTTP layer — the iron-agent
5
+ contract returns HTTP 200 with ``{"status": "failure", "reason": ...}``.
6
+ """
7
+
8
+ from stapel_core.django.api.errors import ErrorKeysView, register_service_errors
9
+
10
+ ERR_400_INVALID_MODEL_SIZE = "error.400.invalid_model_size"
11
+
12
+ AGENT_ERRORS = {
13
+ ERR_400_INVALID_MODEL_SIZE: "Model must be one of: small, medium, large",
14
+ }
15
+
16
+ register_service_errors(AGENT_ERRORS)
17
+
18
+
19
+ class AgentErrorKeysView(ErrorKeysView):
20
+ def get_service_errors(self):
21
+ return AGENT_ERRORS
@@ -0,0 +1,93 @@
1
+ """comm Function providers of the agent module.
2
+
3
+ Registered from ``AgentConfig.ready()`` (importing this module is enough:
4
+ re-imports are no-ops and re-registering the same handler object is
5
+ idempotent). Other modules call these by name via ``stapel_core.comm.call``
6
+ — no import of this package needed, and in a monolith the call is
7
+ in-process without HTTP:
8
+
9
+ from stapel_core.comm import call
10
+
11
+ call("llm.translate", {"from_lang": "auto", "to": "de", "entries": {...}})
12
+ """
13
+ import logging
14
+
15
+ from stapel_core.comm import function
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+ COMPLETE_SCHEMA = {
20
+ "type": "object",
21
+ "properties": {
22
+ "prompt": {"type": "string", "description": "The user prompt."},
23
+ "model": {
24
+ "type": "string",
25
+ "enum": ["small", "medium", "large"],
26
+ "description": "Model size, mapped via STAPEL_AGENT['MODELS'].",
27
+ },
28
+ "system_prompt": {
29
+ "type": "string",
30
+ "description": "Replaces the built-in JSON-API system prompt.",
31
+ },
32
+ "provider": {
33
+ "type": "string",
34
+ "description": "Provider name from STAPEL_AGENT['PROVIDERS'].",
35
+ },
36
+ },
37
+ "required": ["prompt", "model"],
38
+ "additionalProperties": False,
39
+ }
40
+
41
+ TRANSLATE_SCHEMA = {
42
+ "type": "object",
43
+ "properties": {
44
+ "from_lang": {
45
+ "type": "string",
46
+ "description": "Source language code, or 'auto' to auto-detect.",
47
+ },
48
+ "to": {"type": "string", "description": "Target language code."},
49
+ "entries": {
50
+ "type": "object",
51
+ "additionalProperties": {"type": "string"},
52
+ "description": "Mapping of keys to source-language strings.",
53
+ },
54
+ },
55
+ "required": ["from_lang", "to", "entries"],
56
+ "additionalProperties": False,
57
+ }
58
+
59
+
60
+ @function("llm.complete", schema=COMPLETE_SCHEMA)
61
+ def llm_complete(payload: dict) -> dict:
62
+ """JSON LLM completion — same result dict as ``POST api/llm/complete``.
63
+
64
+ Payload: ``{"prompt": str, "model": "small"|"medium"|"large",
65
+ "system_prompt"?: str, "provider"?: str}``. Returns
66
+ ``{"status": "ok"|"failure", "result"?: object, "comment"?: str,
67
+ "reason"?: str, "usage"?: {...}}``.
68
+ """
69
+ from . import services
70
+
71
+ return services.complete_json(
72
+ payload["prompt"],
73
+ payload["model"],
74
+ system_prompt=payload.get("system_prompt"),
75
+ provider=payload.get("provider"),
76
+ )
77
+
78
+
79
+ @function("llm.translate", schema=TRANSLATE_SCHEMA)
80
+ def llm_translate(payload: dict) -> dict:
81
+ """Key-value translation — same result dict as ``POST api/llm/translate``.
82
+
83
+ Payload: ``{"from_lang": str, "to": str, "entries": {key: text}}``.
84
+ Returns ``{"status": "ok", "result": {key: translated}}`` or
85
+ ``{"status": "failure", "reason": str}``.
86
+ """
87
+ from . import services
88
+
89
+ return services.translate(
90
+ payload["from_lang"],
91
+ payload["to"],
92
+ payload["entries"],
93
+ )
@@ -0,0 +1,43 @@
1
+ # Generated by Django 6.0.6 on 2026-07-03 20:16
2
+
3
+ import uuid
4
+ from django.db import migrations, models
5
+
6
+
7
+ class Migration(migrations.Migration):
8
+
9
+ initial = True
10
+
11
+ dependencies = [
12
+ ]
13
+
14
+ operations = [
15
+ migrations.CreateModel(
16
+ name='PromptLog',
17
+ fields=[
18
+ ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
19
+ ('source', models.CharField(choices=[('llm_facade', 'LLM Facade'), ('translate', 'Translate'), ('other', 'Other')], db_index=True, max_length=32)),
20
+ ('model', models.CharField(max_length=128)),
21
+ ('model_size', models.CharField(max_length=16)),
22
+ ('prompt', models.TextField()),
23
+ ('system_prompt', models.TextField(blank=True, null=True)),
24
+ ('response', models.TextField(blank=True, null=True)),
25
+ ('status', models.CharField(choices=[('success', 'Success'), ('failure', 'Failure'), ('timeout', 'Timeout'), ('error', 'Error')], db_index=True, max_length=16)),
26
+ ('error_message', models.TextField(blank=True, null=True)),
27
+ ('input_tokens', models.IntegerField(blank=True, null=True)),
28
+ ('output_tokens', models.IntegerField(blank=True, null=True)),
29
+ ('thinking_tokens', models.IntegerField(blank=True, null=True)),
30
+ ('cache_read_tokens', models.IntegerField(blank=True, null=True)),
31
+ ('cache_write_tokens', models.IntegerField(blank=True, null=True)),
32
+ ('duration_ms', models.IntegerField(blank=True, null=True)),
33
+ ('user_id', models.CharField(blank=True, db_index=True, max_length=64, null=True)),
34
+ ('metadata', models.JSONField(blank=True, null=True)),
35
+ ('created_at', models.DateTimeField(auto_now_add=True, db_index=True)),
36
+ ],
37
+ options={
38
+ 'db_table': 'agent_prompt_log',
39
+ 'ordering': ['-created_at'],
40
+ 'indexes': [models.Index(fields=['source', '-created_at'], name='agent_source_created_idx')],
41
+ },
42
+ ),
43
+ ]
File without changes