"""
A PLugin that uses a remote SQL database server to retrieve its return data.
.. note::
This is a complex AI resource that exists within the following class hierarchy
1. Smarter Secret: The authentication credential for the remote SQL database connection.
2. Smarter SQL Connection: The complete connection configuration to the remote SQL database server (host, port, secret, ssh key, username, etc.).
3. Smarter SQL Plugin: The plugin that defines the SQL query and it's parameters to run against the remote SQL database server.
4. Smarter LLMClient: The prompting resource (LLMClient, Agent, Workflow unit, etcetera) that includes the SQL Plugin:
.. sphinx note: these are relative to the rst doc that calls automodule on this file.
.. literalinclude:: ../../../../../smarter/smarter/apps/account/data/example-manifests/secret-smarter-test-db.yaml
:language: yaml
:caption: 1.) Example Smarter Secret Manifest
.. literalinclude:: ../../../../../smarter/smarter/apps/connection/data/sample-connections/smarter-test-db.yaml
:language: yaml
:caption: 2.) Example Smarter SQL Connection Manifest
.. literalinclude:: ../../../../../smarter/smarter/apps/llmtool/data/stackademy/stackademy-plugin-sql.yaml
:language: yaml
:caption: 3.) Example Stackademy SQL Plugin Manifest
.. literalinclude:: ../../../../../smarter/smarter/apps/llmtool/data/stackademy/stackademy-llmclient-sql.yaml
:language: yaml
:caption: 4.) Example Stackademy LLMClient Manifest
"""
import re
from datetime import datetime
from typing import Optional, Type, Union
from django.core.exceptions import MultipleObjectsReturned
from smarter.apps.connection.models import SqlConnection
from smarter.apps.llmtool.const import PluginCacheTtl
from smarter.apps.llmtool.manifest.enum import (
SAMPluginCommonMetadataClass,
SAMPluginCommonSpecSelectorKeyDirectiveValues,
SAMPluginSpecKeys,
)
from smarter.apps.llmtool.manifest.models.common import Parameter, ParameterType
from smarter.apps.llmtool.manifest.models.common.plugin.metadata import (
SAMPluginCommonMetadata,
)
from smarter.apps.llmtool.manifest.models.common.plugin.spec import (
SAMPluginCommonSpecPrompt,
SAMPluginCommonSpecSelector,
)
from smarter.apps.llmtool.manifest.models.common.plugin.status import (
SAMPluginCommonStatus,
)
from smarter.apps.llmtool.manifest.models.sql_plugin.const import MANIFEST_KIND
from smarter.apps.llmtool.manifest.models.sql_plugin.model import SAMSqlPlugin
from smarter.apps.llmtool.manifest.models.sql_plugin.spec import (
SAMSqlPluginSpec,
SqlData,
TestValue,
)
from smarter.apps.llmtool.models import PluginDataSql, PluginMeta
from smarter.apps.llmtool.serializers import PluginSqlSerializer
from smarter.common.api import SmarterApiVersions
from smarter.common.conf import settings_defaults
from smarter.common.const import SMARTER_ADMIN_USERNAME
from smarter.common.exceptions import SmarterConfigurationError
from smarter.common.utils import to_snake_case
from smarter.lib import json, logging
from smarter.lib.cache import cache_results
from smarter.lib.django import waffle
from smarter.lib.django.waffle import SmarterWaffleSwitches
from smarter.lib.logging import WaffleSwitchedLoggerWrapper
from smarter.lib.manifest.enum import SAMKeys
from smarter.lib.typing import SmarterDictType
from .base import PluginBase, SmarterPluginError
# pylint: disable=W0613
[docs]
def should_log(level):
"""Check if logging should be done based on the waffle switch."""
return waffle.switch_is_active(SmarterWaffleSwitches.PLUGIN_LOGGING)
base_logger = logging.getLogger(__name__)
logger = WaffleSwitchedLoggerWrapper(base_logger, should_log)
MAX_SQL_QUERY_LENGTH = 1000 # Maximum length of SQL query to prevent excessive load on the database
[docs]
class SmarterSqlPluginError(SmarterPluginError):
"""Base class for all SQL plugin errors."""
[docs]
class SqlPlugin(PluginBase):
"""
Implements a plugin that executes SQL queries on a remote SQL database server to retrieve data.
This class provides the logic for integrating SQL-based plugins into the Smarter platform. It supports
manifest-driven configuration, parameter validation, and secure query execution. The plugin can be
instantiated from either a manifest (Pydantic model) or an existing Django ORM instance.
Key Features:
- Accepts plugin configuration via manifest or ORM model.
- Validates and recasts parameter definitions to conform to OpenAI function calling schema.
- Interpolates user-provided arguments into SQL queries, with basic escaping for safety.
- Executes queries using a remote SQL connection and returns results in JSON format.
- Handles errors related to configuration, connection, and query execution.
- Provides example manifest generation for testing and documentation.
Usage:
- Instantiate with a manifest or ORM model.
- Use `tool_call_fetch_plugin_response()` to execute SQL queries with arguments from OpenAI tool calls.
- Use `plugin_data_django_model` to convert manifest data to a Django ORM-compatible dictionary.
- Use `example_manifest()` to generate a sample manifest for this plugin type.
.. note::
- SQL queries are interpolated using simple string replacement; ensure queries are safe and parameterized.
- The maximum query length is limited to prevent excessive database load.
- Logging is controlled via feature switches and log level settings.
.. seealso::
- OpenAI Function Calling: https://platform.openai.com/docs/guides/function-calling?api-mode=prompt
- Smarter Plugin Manifest Documentation
"""
SAMPluginType = SAMSqlPlugin
_manifest: Optional[SAMSqlPlugin] = None
_metadata_class = SAMPluginCommonMetadataClass.SQL.value
_plugin_data: PluginDataSql | None = None
_plugin_data_serializer: PluginSqlSerializer | None = None
[docs]
def __init__(
self,
*args,
manifest: Optional[SAMSqlPlugin] = None,
**kwargs,
):
super().__init__(*args, manifest=manifest, **kwargs)
@property
def kind(self) -> str:
"""
Returns the kind identifier for this plugin.
This property provides the canonical string used to distinguish the plugin type within the Smarter platform.
The value is derived from the plugin manifest constant and is used for registration, serialization, and
manifest validation.
The kind is typically referenced in plugin manifests and API payloads to ensure the correct plugin logic
is invoked for SQL-based plugins.
:returns: The string identifier representing the plugin kind.
:rtype: str
:example:
>>> plugin = SqlPlugin()
>>> plugin.kind
'sql'
"""
return MANIFEST_KIND
@property
def manifest(self) -> Optional[SAMSqlPlugin]:
"""
Retrieve the manifest for this plugin as a Pydantic model.
This property returns the plugin's manifest, which contains the configuration and specification
for the SQL plugin in the form of a validated Pydantic model. If the manifest is not already set
and the plugin is ready, it will be constructed from the current plugin data using the appropriate
Pydantic model class.
The manifest is essential for validating plugin configuration, generating OpenAI-compatible schemas,
and ensuring the plugin operates with the correct parameters and metadata.
:returns: The manifest as a ``SAMSqlPlugin`` Pydantic model instance, or ``None`` if unavailable.
:rtype: Optional[SAMSqlPlugin]
.. note::
If the manifest is not present but the plugin is ready, this property will attempt to reconstruct
the manifest from the plugin's JSON representation.
:example:
>>> plugin = SqlPlugin()
>>> manifest = plugin.manifest
>>> print(manifest.spec.sqlData.sql_query)
SELECT * FROM auth_user WHERE username = '{username}';
"""
if not self._manifest and self.ready:
# if we don't have a manifest but we do have Django ORM data then
# we can work backwards to the Pydantic model
self._manifest = self.SAMPluginType(**self.to_json()) # type: ignore[call-arg]
return self._manifest
@property
def plugin_data(self) -> Optional[PluginDataSql]:
"""
Return the plugin data as a Django ORM instance.
This property provides access to the plugin's data in the form of a Django ORM model instance.
It handles multiple scenarios for retrieving or constructing the plugin data:
- If the plugin data has already been set, it is returned directly.
- If both a manifest and plugin metadata are present, the plugin data is constructed from the manifest and metadata.
- If only plugin metadata is present, the plugin data is retrieved from the database using the metadata.
- If neither is available, the property returns ``None``.
This logic ensures that the plugin data is always consistent with the current manifest and database state,
supporting both creation and update workflows.
:returns: The plugin data as a ``PluginDataSql`` Django ORM instance, or ``None`` if unavailable.
:rtype: Optional[PluginDataSql]
:raises PluginDataSql.DoesNotExist: If the plugin metadata is present but no corresponding database entry exists.
.. note::
This property does not create new database entries; it only retrieves or constructs plugin data
based on existing manifest and metadata.
:example:
>>> plugin = SqlPlugin()
>>> data = plugin.plugin_data
>>> print(data.sql_query)
SELECT * FROM auth_user WHERE username = '{username}';
"""
if self._plugin_data:
return self._plugin_data
try:
self._plugin_data = PluginDataSql.get_cached_object(plugin=self.plugin_meta) # type: ignore[call-arg]
logger.debug(
"%s.plugin_data() retrieved existing PluginDataSql from database.",
self.formatted_class_name,
)
return self._plugin_data
except PluginDataSql.DoesNotExist:
logger.debug(
"%s.plugin_data() no existing PluginDataSql found in database for plugin_meta: %s",
self.formatted_class_name,
self.plugin_meta,
)
# we only want a preexisting manifest ostensibly sourced
# from the cli, not a lazy-loaded
if self._manifest and self.plugin_meta:
# this is an update scenario. the Plugin exists in the database,
# AND we've received manifest data from the cli.
self._plugin_data = PluginDataSql(**self.plugin_data_django_model) # type: ignore[call-arg]
self._plugin_data.save()
logger.debug(
"%s.plugin_data() created new instance of %s from manifest and plugin metadata.",
self.formatted_class_name,
self.plugin_data_class.__name__,
)
if self.plugin_meta:
# we don't have a Pydantic model but we do have an existing
# Django ORM model instance, so we can use that directly.
logger.debug(
"%s.plugin_data() retrieving PluginDataSql from database using plugin metadata.",
self.formatted_class_name,
)
self._plugin_data = PluginDataSql.get_cached_data_by_plugin(
plugin=self.plugin_meta,
)
# new Plugin scenario. there's nothing in the database yet.
return self._plugin_data
@property
def plugin_data_class(self) -> Type[PluginDataSql]:
"""
Return the Django ORM model class used for plugin data.
This property provides the class reference for the Django model that stores
SQL plugin data in the Smarter platform. It is useful for type checking,
serialization, and for constructing new instances of plugin data objects.
The returned class can be used to create, query, or update plugin data
records in the database. This is especially relevant when integrating
with Django's ORM or when performing migrations and schema validation.
:returns: The Django ORM model class for SQL plugin data.
:rtype: Type[PluginDataSql]
:example:
>>> plugin = SqlPlugin()
>>> model_cls = plugin.plugin_data_class
>>> isinstance(model_cls(), PluginDataSql)
True
"""
return PluginDataSql
@property
def plugin_data_serializer(self) -> Optional[PluginSqlSerializer]:
"""
Return the serializer instance for plugin data.
This property provides a serializer object for the SQL plugin's data, allowing
conversion between Django ORM model instances and JSON-compatible representations.
The serializer is used for validating, serializing, and deserializing plugin data,
especially when preparing data for API responses or manifest generation.
If the serializer has not yet been instantiated, it will be created using the
current plugin data. This ensures that the serializer always reflects the latest
state of the plugin's data.
:returns: An instance of ``PluginSqlSerializer`` for the plugin data, or ``None`` if plugin data is unavailable.
:rtype: Optional[PluginSqlSerializer]
:example:
>>> plugin = SqlPlugin()
>>> serializer = plugin.plugin_data_serializer
>>> print(serializer.data)
{'sql_query': "SELECT * FROM auth_user WHERE username = '{username}';", ...}
"""
if not self._plugin_data_serializer:
self._plugin_data_serializer = PluginSqlSerializer(self.plugin_data)
return self._plugin_data_serializer
@property
def plugin_data_serializer_class(self) -> Type[PluginSqlSerializer]:
"""
Return the serializer class used for SQL plugin data.
This property provides the class reference for the serializer that is responsible
for converting SQL plugin data between Django ORM model instances and JSON-compatible
representations. The serializer class is essential for validation, serialization,
and deserialization of plugin data, especially when integrating with APIs or
generating manifests.
Use this property when you need to instantiate a new serializer for SQL plugin data,
perform type checks, or access serializer-specific methods and attributes.
:returns: The serializer class for SQL plugin data.
:rtype: Type[PluginSqlSerializer]
:example:
>>> plugin = SqlPlugin()
>>> serializer_cls = plugin.plugin_data_serializer_class
>>> serializer = serializer_cls(plugin.plugin_data)
>>> isinstance(serializer, PluginSqlSerializer)
True
"""
return PluginSqlSerializer
@property
def plugin_data_django_model(self) -> Optional[SmarterDictType]:
"""
Transform the Pydantic model to the PluginDataSql Django ORM model and return the plugin data definition as a JSON object.
See the OpenAI documentation:
https://platform.openai.com/docs/guides/function-calling?api-mode=prompt
The Pydantic 'Parameters' model is not directly compatible with OpenAI's function calling schema,
and our Django ORM model expects a dictionary format for the parameters. This method converts
the Pydantic model to a dictionary suitable for creating a Django ORM model instance.
:raises SmarterSqlPluginError: If the plugin manifest or SQL data is missing or invalid.
:raises SmarterConfigurationError: If the parameters are not in the expected format.
:returns: A dictionary representing the PluginDataSql Django ORM model.
:rtype: Optional[SmarterDictType]
**Example of a correctly formatted dictionary:**
.. code-block:: python
{
"properties": {
"location": {
"type": "string",
"description": "The city and state, e.g., San Francisco, CA"
},
"unit": {
"type": "string",
"enum": ["Celsius", "Fahrenheit"],
"description": "The temperature unit to use. Infer this from the user's location."
}
},
"required": ["location", "unit"]
}
**Example of a Pydantic model:**
.. code-block:: python
[
{
"name": "max_cost",
"type": "float",
"description": "the maximum cost that a student is willing to pay for a course.",
"required": False,
"enum": None,
"default": None
},
{
"name": "description",
"type": "string",
"description": "areas of specialization for courses in the catalogue.",
"required": False,
"enum": ["AI", "mobile", "web", "database", "network", "neural networks"],
"default": None
}
]
.. code-block:: yaml
spec:
selector:
directive: search_terms
searchTerms:
- admin
- Smarter platform
- admin account
prompt:
provider: openai
systemRole: >
You are a helpful assistant for Smarter platform. You can provide information about the admin account of the Smarter platform.
model: gpt-6-luna
temperature: 0.0
maxTokens: 256
connection: test_sql_connection
sqlData:
sqlQuery: >
SELECT * FROM auth_user WHERE username = '{username}';
parameters:
- name: username
type: string
description: The username to query.
required: true
default: admin
- name: unit
type: string
enum:
- Celsius
- Fahrenheit
description: The temperature unit to use.
required: false
default: Celsius
testValues:
- name: username
value: admin
- name: unit
value: Celsius
limit: 10
"""
if not self._manifest:
return None
if not isinstance(self.plugin_meta, PluginMeta):
logger.debug(
"%s.plugin_data_django_model() plugin_meta is not set. plugin_meta: %s",
self.formatted_class_name,
self.plugin_meta,
)
if not isinstance(self.manifest, SAMSqlPlugin):
raise SmarterSqlPluginError(
f"{self.formatted_class_name}.plugin_data_django_model() error: {self.name} plugin manifest is not available."
)
sql_data = self.manifest.spec.sqlData.model_dump() if self.manifest else None
if not sql_data:
raise SmarterSqlPluginError(
f"{self.formatted_class_name}.plugin_data_django_model() error: {self.name} missing required SQL data."
)
sql_data = {to_snake_case(key): value for key, value in sql_data.items()}
connection_name = self._manifest.spec.connection if self._manifest else None
if connection_name:
# recast the Pydantic model to the PluginDataSql Django ORM model
plugin_data_sqlconnection: Optional[SqlConnection] = None
account = self.user_profile.cached_account if self.user_profile else None
try:
plugin_data_sqlconnection = SqlConnection.objects.get(
user_profile__account=account,
name=connection_name,
)
except SqlConnection.DoesNotExist:
pass
except MultipleObjectsReturned:
try:
# narrow the search to exactly the user_profile
# that authenticated the request.
plugin_data_sqlconnection = SqlConnection.objects.get(
user_profile=self.user_profile,
name=connection_name,
)
except SqlConnection.DoesNotExist:
pass
if not plugin_data_sqlconnection:
raise SmarterSqlPluginError(
f"{self.formatted_class_name}.plugin_data_django_model() error: SqlConnection {connection_name} does not exist for account {account}."
)
plugin_data_sqlconnection.authorize()
sql_data["connection"] = plugin_data_sqlconnection
# recast the Pydantic model's parameters field
# to conform to openai's function calling schema.
recasted_parameters = {"type": "object", "properties": {}, "required": [], "additionalProperties": False}
parameters = self.manifest.spec.sqlData.parameters if self.manifest and self.manifest.spec else None
if parameters and not isinstance(parameters, list):
raise SmarterConfigurationError(
f"{self.formatted_class_name}.plugin_data_django_model() error: {self.name} parameters must be a list of dictionaries. Received: {parameters} {type(parameters)}"
)
if parameters:
logger.debug("%s.plugin_data_django_model() recasting parameters", self.formatted_class_name)
for parameter in parameters:
if not isinstance(parameter, Parameter):
raise SmarterConfigurationError(
f"{self.formatted_class_name}.plugin_data_django_model() error: {self.name} each parameter must be a Pydantic Parameter model. Received: {parameter} {type(parameter)}"
)
# if the parameter is a Pydantic model, we need to convert it to a
# standard json dict.
parameter = parameter.model_dump()
logger.debug(
"%s.plugin_data_django_model() processing parameter: %s",
self.formatted_class_name,
parameter,
)
if not isinstance(parameter, dict):
raise SmarterConfigurationError(
f"{self.formatted_class_name}.plugin_data_django_model() error: {self.name} each parameter must be a valid json dict. Received: {parameter} {type(parameter)}"
)
if "name" not in parameter or "type" not in parameter:
raise SmarterConfigurationError(
f"{self.formatted_class_name}.plugin_data_django_model() error: {self.name} each parameter must have a 'name' and 'type' field. Received: {parameter}"
)
recasted_parameters["properties"][parameter["name"]] = {
"type": parameter["type"],
"description": parameter.get("description", ""),
}
if "enum" in parameter and parameter["enum"]:
if not isinstance(parameter["enum"], list):
raise SmarterConfigurationError(
f"{self.formatted_class_name}.plugin_data_django_model() error: {self.name} parameter 'enum' must be a list. Received: {parameter['enum']} {type(parameter['enum'])}"
)
recasted_parameters["properties"][parameter["name"]]["enum"] = parameter["enum"]
if parameter.get("required", False):
recasted_parameters["required"].append(parameter["name"])
if "default" in parameter and parameter["default"] is not None:
recasted_parameters["properties"][parameter["name"]]["default"] = parameter["default"]
sql_data["parameters"] = recasted_parameters
return {
"plugin": self.plugin_meta,
"description": (
self.manifest.metadata.description
if self.manifest and self.manifest.metadata
else self.plugin_meta.description if self.plugin_meta else "no description"
),
**sql_data,
} # type: ignore[call-arg]
[docs]
@classmethod
def example_manifest(cls, kwargs: Optional[SmarterDictType] = None) -> SmarterDictType:
"""
Use Pydantic models to generate an example manifest for SqlPlugin.
This class method creates a sample manifest dictionary for the SqlPlugin,
illustrating the required structure and fields. The example manifest includes
metadata, specification, and SQL query details, demonstrating how to configure
the plugin for use within the Smarter platform.
:param kwargs: Optional dictionary of additional fields to include in the manifest.
:returns: A dictionary representing the example manifest for the SqlPlugin.
:rtype: dict
See Also:
- :py:class:`smarter.lib.manifest.enum.SAMKeys`
- :py:class:`smarter.lib.manifest.enum.SAMMetadataKeys`
- :py:class:`smarter.apps.llmtool.manifest.models.sql_plugin.model.SAMSqlPlugin`
- :py:class:`smarter.apps.llmtool.manifest.models.common.plugin.enum.SAMPluginCommonMetadataClassValues`
- :py:class:`smarter.apps.llmtool.manifest.models.common.plugin.enum.SAMPluginCommonSpecSelectorKeyDirectiveValues`
- :py:class:`smarter.apps.llmtool.manifest.models.common.plugin.enum.SAMPluginCommonSpecPromptKeys`
"""
# build out the sub-models first
metadata = SAMPluginCommonMetadata(
name="sql_example",
pluginClass=SAMPluginCommonMetadataClass.SQL.value,
description="Get additional information about courses available at Stackademy.",
version="0.1.0",
tags=["db", "sql", "database"],
annotations=[
{"smarter.sh/created_by": "smarter_sql_plugin_broker"},
{"smarter.sh/plugin": "sql_example"},
],
)
selector = SAMPluginCommonSpecSelector(
directive=SAMPluginCommonSpecSelectorKeyDirectiveValues.SEARCHTERMS.value,
searchTerms=[
"smarter",
"users",
SMARTER_ADMIN_USERNAME,
],
)
prompt = SAMPluginCommonSpecPrompt(
provider=settings_defaults.LLM_DEFAULT_PROVIDER,
systemRole="You are a helpful sales agent for Stackademy. You can provide information about courses available at Stackademy.\n",
model=settings_defaults.LLM_DEFAULT_MODEL,
temperature=settings_defaults.LLM_DEFAULT_TEMPERATURE,
maxTokens=settings_defaults.LLM_DEFAULT_MAX_TOKENS,
)
connection = "example_connection"
sql_data = SqlData(
sqlQuery="SELECT c.course_code, c.course_name, c.description, c.cost, prerequisite.course_code AS prerequisite_course_code, prerequisite.course_name AS prerequisite_course_name FROM courses c LEFT JOIN courses prerequisite ON c.prerequisite_id = prerequisite.course_id WHERE ((c.description LIKE CONCAT('%', {description}, '%')) OR ({description} IS NULL)) AND (c.cost <= {max_cost} OR {max_cost} IS NULL) ORDER BY c.prerequisite_id;\n",
parameters=[
Parameter(
name="max_cost",
type=ParameterType.NUMBER,
description="the maximum cost that a student is willing to pay for a course.",
required=False,
enum=None,
default=None,
),
Parameter(
name="description",
type=ParameterType.STRING,
description="areas of specialization for courses in the catalogue.",
required=False,
enum=["AI", "mobile", "web", "database", "network", "neural networks"],
default=None,
),
],
testValues=[
TestValue(
name="description",
value="AI",
),
TestValue(
name="max_cost",
value="500",
),
],
limit=10,
)
spec = SAMSqlPluginSpec(
selector=selector,
prompt=prompt,
connection=connection,
sqlData=sql_data,
)
status = SAMPluginCommonStatus(
accountNumber="0123456789",
username=SMARTER_ADMIN_USERNAME,
recordLocator="example_record_locator",
created=datetime(2024, 1, 1, 0, 0, 0),
modified=datetime(2024, 1, 1, 0, 0, 0),
)
# build the full Pydantic model from the sub-models
sam_sql_plugin = SAMSqlPlugin(
apiVersion=SmarterApiVersions.V1,
kind=MANIFEST_KIND,
metadata=metadata,
spec=spec,
status=status,
)
return json.loads(sam_sql_plugin.model_dump_json())
[docs]
def create(self):
"""
Create the plugin in the database.
This method handles the creation of the SQL plugin in the database by invoking
the superclass's create method. It ensures that all necessary data is properly
initialized and stored according to the plugin's configuration.
.. note::
This method currently does not implement any additional logic beyond calling
the superclass's create method.
:returns: None
:rtype: None
"""
logger.debug("%s.create() called.", self.formatted_class_name)
super().create()
# pylint: disable=too-many-branches,too-many-statements
[docs]
def to_json(self, version: str = "v1") -> Optional[SmarterDictType]:
"""
Serialize the SqlPlugin instance to a JSON-compatible dictionary suitable for Pydantic import.
This method transforms the plugin's internal state, including its manifest and data,
into a dictionary format that can be used to instantiate a Pydantic model. This is
primarily used for rendering a plugin manifest for the Smarter API, enabling
interoperability between Django ORM models and Pydantic schemas.
The output includes all relevant plugin fields, with the SQL data section populated
from the plugin's serializer. This ensures that the manifest is complete and
conforms to the expected schema for API consumption or further validation.
:param version: The API version to use for serialization. Only "v1" is supported.
:type version: str
:returns: A dictionary representing the plugin in JSON format, or ``None`` if the plugin is not ready.
:rtype: Optional[SmarterDictType]
:raises SmarterPluginError: If the plugin is not ready, the data is not a valid JSON object, or an unsupported version is specified.
:note:
The returned dictionary is structured for compatibility with Pydantic models and
the Smarter API manifest specification. The SQL data is injected from the plugin's
serializer to ensure accurate representation.
:example:
>>> plugin = SqlPlugin()
>>> manifest_json = plugin.to_json()
>>> print(manifest_json["spec"]["sqlData"]["sql_query"])
SELECT * FROM auth_user WHERE username = '{username}';
"""
if self.ready:
if version == "v1":
retval = super().to_json(version=version)
if not retval:
raise SmarterPluginError(
f"{self.formatted_class_name}.to_json() error: {self.name} plugin is not ready."
)
if not isinstance(retval, dict):
raise SmarterPluginError(
f"{self.formatted_class_name}.to_json() error: {self.name} plugin data is not a valid JSON object. Received: {type(retval)}"
)
retval[SAMKeys.SPEC.value][SAMPluginSpecKeys.SQL_DATA.value] = (
self.plugin_data_serializer.data if self.plugin_data_serializer else None
)
# parameters are stored in OpenAI function calling schema. convert them back to manifest format.
retval[SAMKeys.SPEC.value][SAMPluginSpecKeys.SQL_DATA.value]["parameters"] = (
self.parameters_to_manifest(self.plugin_data.parameters if self.plugin_data else None)
)
# the connection is a top-level spec field in the manifest, not a field of the data section.
retval[SAMKeys.SPEC.value]["connection"] = (
self.plugin_data.connection.name if self.plugin_data and self.plugin_data.connection else None
)
return json.loads(json.dumps(retval))
raise SmarterPluginError(f"Invalid version: {version}")
return None