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
3 changes: 3 additions & 0 deletions src/blacki/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,15 @@ def create_app(agent: LlmAgent | None = None) -> App:
if agent is None:
agent = create_agent()

from blacki.declarative_db.plugin import DeclarativeDbPlugin

return App(
name="blacki",
root_agent=agent,
plugins=[
TelegramModelOverridePlugin(name="telegram_model_override"),
GlobalInstructionPlugin(return_global_instruction),
DeclarativeDbPlugin(name="declarative_db"),
LoggingPlugin(),
],
events_compaction_config=None,
Expand Down
20 changes: 20 additions & 0 deletions src/blacki/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
import aiosqlite

from blacki.calories.storage import SqliteCalorieStorage
from blacki.declarative_db.storage import SqliteDeclarativeDbStorage
from blacki.reminders.storage import SqliteReminderStorage
from blacki.utils.preferences import SqlitePreferencesStorage
from blacki.workouts.storage import SqliteWorkoutStorage
Expand Down Expand Up @@ -134,6 +135,9 @@ class AppContainer:
_preferences_storage: SqlitePreferencesStorage | None = field(
default=None, init=False, repr=False
)
_declarative_db_storage: SqliteDeclarativeDbStorage | None = field(
default=None, init=False, repr=False
)

@classmethod
async def create(cls, sqlite_path: str | Path) -> Self:
Expand Down Expand Up @@ -174,6 +178,10 @@ async def _close_storages(self) -> None:
await self._preferences_storage.close()
self._preferences_storage = None

if self._declarative_db_storage is not None:
await self._declarative_db_storage.close()
self._declarative_db_storage = None

async def initialize_all_storages(self) -> None:
"""Initialize all storage instances.

Expand All @@ -184,6 +192,7 @@ async def initialize_all_storages(self) -> None:
await self.calorie_storage.initialize()
await self.workout_storage.initialize()
await self.preferences_storage.initialize()
await self.declarative_db_storage.initialize()

@property
def lock(self) -> asyncio.Lock:
Expand Down Expand Up @@ -225,3 +234,14 @@ def preferences_storage(self) -> SqlitePreferencesStorage:

self._preferences_storage = SqlitePreferencesStorage(self.conn, self._lock)
return self._preferences_storage

@property
def declarative_db_storage(self) -> SqliteDeclarativeDbStorage:
"""Get or create the declarative database storage instance."""
if self._declarative_db_storage is None:
from blacki.declarative_db.storage import SqliteDeclarativeDbStorage

self._declarative_db_storage = SqliteDeclarativeDbStorage(
self.conn, self._lock
)
return self._declarative_db_storage
58 changes: 58 additions & 0 deletions src/blacki/declarative_db/plugin.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
"""ADK plugin for injecting declarative database instructions."""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from google.adk.plugins.base_plugin import BasePlugin

from blacki.declarative_db.storage import get_declarative_db_storage

if TYPE_CHECKING:
from google.adk.agents.callback_context import CallbackContext
from google.adk.models.llm_request import LlmRequest

logger = logging.getLogger(__name__)


class DeclarativeDbPlugin(BasePlugin):
"""ADK plugin to dynamically load declarative database schemas.

Loads both schemas and custom instruction overrides.
This plugin queries the storage layer using the active session user's ID
and appends user-defined tables, query templates, and instruction overrides
to the system instruction context on every LLM call.
"""

def __init__(self, name: str = "declarative_db") -> None:
super().__init__(name=name)

async def before_model_callback(
self, *, callback_context: CallbackContext, llm_request: LlmRequest
) -> None:
"""Callback executed before the LLM is invoked."""
if not callback_context.session:
return

user_id = callback_context.session.state.get(
"user_id"
) or callback_context.session.state.get("telegram_chat_id")
if not user_id:
logger.debug(
"before_model_callback: No user_id or telegram_chat_id in session state"
)
return

try:
storage = get_declarative_db_storage()
schema_xml = await storage.get_schema_instructions_xml(str(user_id))
if schema_xml:
logger.info(
"Injecting custom database instructions for user %s (%d chars)",
user_id,
len(schema_xml),
)
llm_request.append_instructions([schema_xml])
except Exception:
logger.exception("Failed to inject user database schemas and instructions")
Loading
Loading