Configuring Services¶
Three approaches to wire up services (LLM, tools, knowledge, memory, etc.) in Xyberos, from simplest to most decoupled.
The runnable version lives at examples/configuring_services.py.
1. Explicit — pass instances at construction time¶
The simplest approach: build your providers and hand them directly to
create_app() or Xyberos(). Great for scripts, notebooks, and early
prototyping.
from xyberos import Xyberos, create_app
from xyberos.llm import CallableLLM
from xyberos.knowledge import InMemoryKnowledge
# Convenience helper — fills in defaults for anything you omit
app = create_app(
llm=CallableLLM(lambda prompt: f"response: {prompt}"),
knowledge=InMemoryKnowledge({"hours": "9am-6pm"}),
)
print(app.chat("What are your hours?"))
# Direct construction — full control, no defaults injected
manual = Xyberos(
llm=CallableLLM(lambda prompt: f"custom: {prompt}"),
)
print(manual.chat("hello"))
Pros and cons¶
- Pros: trivial to read, no indirection, everything in one place.
- Cons: the app is coupled to the concrete providers; swapping a backend means changing the creation site.
2. Class / factory — register with the kernel, resolved via DI¶
Register a factory callable with the kernel. The kernel inspects the factory's parameter names and resolves them from the registry (config, logger, or any other registered service). This is how you make a service configurable without touching the app creation site.
from xyberos import create_app
from xyberos.llm import CallableLLM, EchoLLM
def build_llm(config):
"""Factory: config is injected by name from the kernel registry."""
provider = config.get("llm_provider", "echo")
if provider == "echo":
return EchoLLM()
return CallableLLM(lambda prompt: f"[{provider}] {prompt}")
app = create_app(config={"llm_provider": "openai"})
app.register_factory("llm", build_llm, replace=True)
# resolve() picks up the factory-built provider
llm = app.resolve("llm")
print(llm.generate("hi")) # prints: [openai] hi
# Change config at runtime — resolve picks up the new value
app.config.set("llm_provider", "claude")
llm = app.resolve("llm")
print(llm.generate("hi")) # prints: [claude] hi
Note:
app.chat()uses theBrain, which captures its provider references at construction time. Replacing a provider viaregister/register_factoryaffects futureresolve()calls but not the already-built brain.app.load_entry_points()re-syncs the brain's providers after plugin discovery; otherwise, build a fresh app.
Dependency injection by parameter name¶
The kernel's inject() method resolves any callable by matching its
parameter names to registered services:
app = create_app()
app.register("greeting", "hello from service")
# "greeting" matches the registered service name — injected automatically
def build_message(greeting, config):
return f"{greeting} (env: {config.get('env', 'dev')})"
msg = app.inject(build_message) # no args — resolved by name
Pros and cons¶
- Pros: config-driven, swappable at runtime, testable (mock the factory).
- Cons: factories run inside the kernel lifecycle; DI parameter naming must match registered service names.
3. Plugin — auto-discovered, zero wiring in the app¶
Package your services as a Plugin and let auto-discovery load them. The app
never imports the plugin by name — it just calls load_plugins_from() or
load_entry_points().
Convention scan¶
Drop a module in a package folder. Wiring:
# app/plugins/llm_plugin.py
from xyberos.contracts import Plugin
from xyberos.llm import CallableLLM
class LLMPlugin(Plugin):
@property
def name(self):
return "llm_plugin"
def register(self, kernel):
kernel.register("llm", CallableLLM(lambda p: f"plugin: {p}"), replace=True)
def unregister(self, kernel):
kernel.registry.unregister("llm")
# main.py — no import of the plugin!
from xyberos import create_app
app = create_app()
app.load_plugins_from("app.plugins") # auto-discovers LLMPlugin
print(app.chat("hello")) # plugin: hello
Entry points¶
Declare the plugin in the package's pyproject.toml, install it, and the app
discovers it via importlib.metadata — same mechanism pytest and uvicorn use:
# my_chat_lib/pyproject.toml
[project.entry-points."xyberos.plugins"]
llm = "my_chat_lib.plugins:LLMPlugin"
# In any app that has my_chat_lib installed:
app = create_app()
app.load_entry_points() # auto-discovers every installed xyberos.plugins entry point
Pros and cons¶
- Pros: fully decoupled — the app knows nothing about the plugin until discovery runs. Adding or removing a plugin means adding or removing a module (convention) or a package (entry points).
- Cons: requires the
Plugincontract (register/unregister). Best for reusable service bundles, not one-off configuration.
When to use each¶
| Approach | Best for |
|---|---|
| Explicit | Scripts, notebooks, quick prototyping |
| Factory | Config-driven apps, hot-swappable services |
| Plugin | Reusable packages, large apps with many service bundles, third-party extensions |
You can mix them — use create_app(llm=...) for the default and plugins for
the rest, or start explicit and extract plugins as the app grows.