Core agentic LLM layer for EEA GenAI packages.
Provides the multi-provider LLM client, the agent executor (built on
pydantic-ai), the unified enricher interface, MCP toolset integration,
and the control-panel-backed settings shared by all eea.genai.*
packages.
Contents
ILLMClientutility — multi-provider model wrapper. Supported providers:openai-compatible(LiteLLM, vLLM, any OpenAI API),openai,anthropic,google,ollama. Configured via control panel or env vars (LLM_MODEL,LLM_URL,LLM_API_KEY).IAgentExecutorutility — runs apydantic_ai.Agentloop with ZCA-discovered tools, MCP toolsets, structured output, and the enricher pipeline.IEnrichernamed utility interface — single abstraction for reusable system/user prompt fragments. Replaces the previousIAgentSkill+IAgentContextProvidersplit (both kept as aliases).IAgentToolnamed utility interface — tool callables exposed to the LLM viapydantic_aitool calling. Thread-safe Plone site restoration via thesite_scopecontext manager.IAgentConfigurationnamed utility interface + base class — ship default agents from Python via ZCML; the control panelagents_jsonoverrides them by name.- Pure-function prompt composition (
core.prompts.build_prompts) testable without Plone bootstrap. - MCP server integration with
${VAR}and${VAR:-default}env expansion, filtered tool refs (server/tool) and full toolset inclusion viamcp_servers. - Typed exceptions (
AgentDisabled,AgentNotFound,AgentConfigInvalid,AgentExecutionFailed,EnricherFailed) instead of bareRuntimeError. validate_agent_config(config)— fail-fast validation of tool, enricher, MCP server, and output-type references at config load time.- Built-in tools:
extract_blocks,memory,code_exec,fetch_url. eea.genai.managepermission for administrative operations.
- Add
eea.genai.coreto yourrequirements.txt. - Install the GenericSetup profile to enable the control panel.
Packages can contribute a schema to the existing GenAI controlpanel by
registering a named utility implementing IGenAISettingsSection:
<utility
name="image"
factory=".settings.ImageAISettingsSection"
provides="eea.genai.core.interfaces.IGenAISettingsSection"
/>
The utility supplies a schema and get_public_settings() method. Both
classic and REST controlpanels combine the contributed schemas with the core
schema. Field names must be unique across sections. Each package installs its
own records with the existing core prefix:
<records interface="my.package.interfaces.IMySettings"
prefix="eea.genai.core.interfaces.IGenAISettings" />
This preserves existing core registry keys and avoids core depending on feature
packages. Import newly contributed registry records before opening the extended
controlpanel on an existing site. See docs/ADR-001-settings-sections.md for
the storage decision.
Public GET /@genai-settings returns only the master switch and explicit
package feature payloads:
{"enabled": true, "features": {"image": {"enabled": false}}}
get_public_settings() must return only allowlisted, JSON-compatible values
safe for anonymous reading and serialization into SSR HTML. Do not include
provider configuration, prompts, MCP configuration, environment values, or
credentials. Frontends must combine the master and feature switches. The
administrative @controlpanels/genai endpoint remains separate.
LLM_MODEL— model identifier (e.g.gpt-4o,claude-sonnet-4-6,llama3). Falls back here if the registry value is empty.LLM_URL— base URL for OpenAI-compatible or Ollama endpoints (e.g.http://localhost:4000/v1). Not used for direct OpenAI/Anthropic/Google providers.LLM_API_KEY— API key for the configured provider.ANTHROPIC_API_KEY,GOOGLE_API_KEY— provider-specific overrides for direct provider mode.
API keys are never stored in the registry.
Define an enricher:
from eea.genai.core.interfaces import Enricher
class MyContextEnricher(Enricher):
description = "Adds dataset summary to the user prompt"
def user_prompt(self, deps):
return f"Active dataset: {deps.context.title}"
Register it via ZCML:
<genai:agentSkill name="my_context" class=".enrichers.MyContextEnricher" />
Define an agent:
from eea.genai.core.interfaces import AgentConfiguration
class MyAgent(AgentConfiguration):
system_prompt = "You are an analyst."
task_prompt = "Produce a one-paragraph briefing."
enrichers = ["my_context", "generic_metadata"]
tools = ["fetch_url"]
output_type = "my.package.models.Briefing"
max_iterations = 5
Register it:
<genai:agent name="briefing" class=".agents.MyAgent" />
Run it:
from eea.genai.core.agent import AgentDeps
from eea.genai.core.utils import get_executor
result = get_executor().run_with_agent(
"briefing",
user_prompt="Brief me on regional air quality.",
deps=AgentDeps(context=obj, request=request),
)
Control-panel agents_json overrides Python-defined agents by name
without code changes.
See ARCHITECTURE.md in this package for the full reference.
The Initial Owner of the Original Code is European Environment Agency (EEA). All Rights Reserved.
All contributions to this package are property of their respective authors, and are covered by the same license.
The eea.genai.core is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 2 of the License, or (at your option) any later version.
This repository uses the Betterleaks GitHub Action to scan the current
repository content on every push and pull request. The scan uses the rules in
.gitleaks.toml and uploads a betterleaks-report artifact when a finding
is detected.
If the optional SMTP secrets are configured, failed scans also send an email to the last commit committer. The workflow expects these repository or organization secrets:
SMTP_URLSMTP_PORT(optional, defaults to25)SMTP_EMAILSMTP_PASSWORD(optional if the SMTP server does not require authentication)
Port 465 is sent with direct TLS; other ports use the default SMTP
handshake. The email includes a short finding summary from the redacted
Betterleaks report, including the redacted matched line from each finding.
There are three common outcomes:
Everything is OK. The
Betterleaks / Scan for secretscheck is green and no action is needed. Regular references to runtime values are OK, for example:token_from_cookie = request.cookies.get("auth_token")A real secret was found. The check is red and the workflow log asks you to download the
betterleaks-reportartifact. Open the artifact from the GitHub Actions run and check the reported file, line and rule. Remove the committed value, move it to the proper secret store, and rotate it if it was exposed. A report entry looks like this:{ "RuleID": "secret-literal-assignment", "File": "src/config.py", "StartLine": 12, "Secret": "[REDACTED]" }The finding is a false positive. Keep the value only if it is clearly not sensitive, such as a test fixture, placeholder, or public example. Add
betterleaks:allowon the same line and include a short explanation in the pull request:test_password = "admin" #betterleaks:allow
Do not add betterleaks:allow to real credentials.