diff --git a/README.md b/README.md
index 3eee92e..7b115e7 100644
--- a/README.md
+++ b/README.md
@@ -7,7 +7,7 @@
## Introduction
-The Universal Tool Calling Protocol (UTCP) is a modern, flexible, and scalable standard for defining and interacting with tools across a wide variety of communication protocols. UTCP 1.0.0 introduces a modular core with a plugin-based architecture, making it more extensible, testable, and easier to package.
+The Universal Tool Calling Protocol (UTCP) is a secure, scalable standard for defining and interacting with tools across a wide variety of communication protocols. UTCP 1.0.0 introduces a modular core with a plugin-based architecture, making it more extensible, testable, and easier to package.
In contrast to other protocols, UTCP places a strong emphasis on:
@@ -87,7 +87,7 @@ Version 1.0.0 introduces several breaking changes. Follow these steps to migrate
3. **Update Imports**: Change your imports to reflect the new modular structure. For example, `from utcp.client.transport_interfaces.http_transport import HttpProvider` becomes `from utcp_http.http_call_template import HttpCallTemplate`.
4. **Tool Search**: If you were using the default search, the new strategy is `TagAndDescriptionWordMatchStrategy`. This is the new default and requires no changes unless you were implementing a custom strategy.
5. **Tool Naming**: Tool names are now namespaced as `manual_name.tool_name`. The client handles this automatically.
-6 **Variable Substitution Namespacing**: Variables that are subsituted in different `call_templates`, are first namespaced with the name of the manual with the `_` duplicated. So a key in a tool call template called `API_KEY` from the manual `manual_1` would be converted to `manual__1_API_KEY`.
+6. **Variable Substitution Namespacing**: Variables that are substituted in different `call_templates`, are first namespaced with the name of the manual with the `_` duplicated. So a key in a tool call template called `API_KEY` from the manual `manual_1` would be converted to `manual__1_API_KEY`.
## Usage Examples
@@ -226,7 +226,7 @@ if __name__ == "__main__":
### 2. Providing a UTCP Manual
-A `UTCPManual` describes the tools you offer. The key change is replacing `tool_provider` with `call_template`.
+A `UTCPManual` describes the tools you offer. The key change is replacing `tool_provider` with `tool_call_template`.
**`server.py`**
@@ -288,7 +288,7 @@ def utcp_discovery():
"conditions": {"type": "string"}
}
},
- "call_template": {
+ "tool_call_template": {
"call_template_type": "http",
"url": "https://example.com/api/weather",
"http_method": "GET"
@@ -311,7 +311,7 @@ You can find full examples in the [examples repository](https://github.com/unive
### `UtcpManual` and `Tool` Models
-The `tool_provider` object inside a `Tool` has been replaced by `call_template`.
+The `tool_provider` object inside a `Tool` has been replaced by `tool_call_template`.
```json
{
@@ -324,7 +324,7 @@ The `tool_provider` object inside a `Tool` has been replaced by `call_template`.
"inputs": { ... },
"outputs": { ... },
"tags": ["string"],
- "call_template": {
+ "tool_call_template": {
"call_template_type": "http",
"url": "https://...",
"http_method": "GET"
diff --git a/core/src/utcp/data/auth.py b/core/src/utcp/data/auth.py
index 875ece9..7436e32 100644
--- a/core/src/utcp/data/auth.py
+++ b/core/src/utcp/data/auth.py
@@ -11,7 +11,8 @@
import traceback
class Auth(BaseModel, ABC):
- """Authentication details for a provider.
+ """REQUIRED
+ Authentication details for a provider.
Attributes:
auth_type: The authentication type identifier.
@@ -19,12 +20,39 @@ class Auth(BaseModel, ABC):
auth_type: str
class AuthSerializer(Serializer[Auth]):
+ """REQUIRED
+ Serializer for authentication details.
+
+ Defines the contract for serializers that convert authentication details to and from
+ dictionaries for storage or transmission. Serializers are responsible for:
+ - Converting authentication details to dictionaries for storage or transmission
+ - Converting dictionaries back to authentication details
+ - Ensuring data consistency during serialization and deserialization
+ """
auth_serializers: dict[str, Serializer[Auth]] = {}
def to_dict(self, obj: Auth) -> dict:
+ """REQUIRED
+ Convert an Auth object to a dictionary.
+
+ Args:
+ obj: The Auth object to convert.
+
+ Returns:
+ The dictionary converted from the Auth object.
+ """
return AuthSerializer.auth_serializers[obj.auth_type].to_dict(obj)
def validate_dict(self, obj: dict) -> Auth:
+ """REQUIRED
+ Validate a dictionary and convert it to an Auth object.
+
+ Args:
+ obj: The dictionary to validate and convert.
+
+ Returns:
+ The Auth object converted from the dictionary.
+ """
try:
return AuthSerializer.auth_serializers[obj["auth_type"]].validate_dict(obj)
except KeyError:
diff --git a/core/src/utcp/data/auth_implementations/api_key_auth.py b/core/src/utcp/data/auth_implementations/api_key_auth.py
index 47c6ddb..a614afd 100644
--- a/core/src/utcp/data/auth_implementations/api_key_auth.py
+++ b/core/src/utcp/data/auth_implementations/api_key_auth.py
@@ -5,7 +5,8 @@
from utcp.exceptions import UtcpSerializerValidationError
class ApiKeyAuth(Auth):
- """Authentication using an API key.
+ """REQUIRED
+ Authentication using an API key.
The key can be provided directly or sourced from an environment variable.
Supports placement in headers, query parameters, or cookies.
@@ -30,10 +31,30 @@ class ApiKeyAuth(Auth):
class ApiKeyAuthSerializer(Serializer[ApiKeyAuth]):
+ """REQUIRED
+ Serializer for ApiKeyAuth model."""
def to_dict(self, obj: ApiKeyAuth) -> dict:
+ """REQUIRED
+ Convert an ApiKeyAuth object to a dictionary.
+
+ Args:
+ obj: The ApiKeyAuth object to convert.
+
+ Returns:
+ The dictionary converted from the ApiKeyAuth object.
+ """
return obj.model_dump()
def validate_dict(self, obj: dict) -> ApiKeyAuth:
+ """REQUIRED
+ Validate a dictionary and convert it to an ApiKeyAuth object.
+
+ Args:
+ obj: The dictionary to validate and convert.
+
+ Returns:
+ The ApiKeyAuth object converted from the dictionary.
+ """
try:
return ApiKeyAuth.model_validate(obj)
except ValidationError as e:
diff --git a/core/src/utcp/data/auth_implementations/basic_auth.py b/core/src/utcp/data/auth_implementations/basic_auth.py
index 4d09937..075e208 100644
--- a/core/src/utcp/data/auth_implementations/basic_auth.py
+++ b/core/src/utcp/data/auth_implementations/basic_auth.py
@@ -5,7 +5,8 @@
from utcp.exceptions import UtcpSerializerValidationError
class BasicAuth(Auth):
- """Authentication using HTTP Basic Authentication.
+ """REQUIRED
+ Authentication using HTTP Basic Authentication.
Uses the standard HTTP Basic Authentication scheme with username and password
encoded in the Authorization header.
@@ -22,10 +23,30 @@ class BasicAuth(Auth):
class BasicAuthSerializer(Serializer[BasicAuth]):
+ """REQUIRED
+ Serializer for BasicAuth model."""
def to_dict(self, obj: BasicAuth) -> dict:
+ """REQUIRED
+ Convert a BasicAuth object to a dictionary.
+
+ Args:
+ obj: The BasicAuth object to convert.
+
+ Returns:
+ The dictionary converted from the BasicAuth object.
+ """
return obj.model_dump()
def validate_dict(self, obj: dict) -> BasicAuth:
+ """REQUIRED
+ Validate a dictionary and convert it to a BasicAuth object.
+
+ Args:
+ obj: The dictionary to validate and convert.
+
+ Returns:
+ The BasicAuth object converted from the dictionary.
+ """
try:
return BasicAuth.model_validate(obj)
except ValidationError as e:
diff --git a/core/src/utcp/data/auth_implementations/oauth2_auth.py b/core/src/utcp/data/auth_implementations/oauth2_auth.py
index cd178b7..43f8c1d 100644
--- a/core/src/utcp/data/auth_implementations/oauth2_auth.py
+++ b/core/src/utcp/data/auth_implementations/oauth2_auth.py
@@ -6,7 +6,8 @@
class OAuth2Auth(Auth):
- """Authentication using OAuth2 client credentials flow.
+ """REQUIRED
+ Authentication using OAuth2 client credentials flow.
Implements the OAuth2 client credentials grant type for machine-to-machine
authentication. The client automatically handles token acquisition and refresh.
@@ -27,10 +28,30 @@ class OAuth2Auth(Auth):
class OAuth2AuthSerializer(Serializer[OAuth2Auth]):
+ """REQUIRED
+ Serializer for OAuth2Auth model."""
def to_dict(self, obj: OAuth2Auth) -> dict:
+ """REQUIRED
+ Convert an OAuth2Auth object to a dictionary.
+
+ Args:
+ obj: The OAuth2Auth object to convert.
+
+ Returns:
+ The dictionary converted from the OAuth2Auth object.
+ """
return obj.model_dump()
def validate_dict(self, obj: dict) -> OAuth2Auth:
+ """REQUIRED
+ Validate a dictionary and convert it to an OAuth2Auth object.
+
+ Args:
+ obj: The dictionary to validate and convert.
+
+ Returns:
+ The OAuth2Auth object converted from the dictionary.
+ """
try:
return OAuth2Auth.model_validate(obj)
except ValidationError as e:
diff --git a/core/src/utcp/data/call_template.py b/core/src/utcp/data/call_template.py
index f79acdd..eff0f1b 100644
--- a/core/src/utcp/data/call_template.py
+++ b/core/src/utcp/data/call_template.py
@@ -29,7 +29,8 @@
from utcp.data.auth import Auth, AuthSerializer
class CallTemplate(BaseModel):
- """Base class for all UTCP tool providers.
+ """REQUIRED
+ Base class for all UTCP tool providers.
This is the abstract base class that all specific call template implementations
inherit from. It provides the common fields that every provider must have.
@@ -61,12 +62,39 @@ def validate_auth(cls, v: Optional[Union[Auth, dict]]):
return AuthSerializer().validate_dict(v)
class CallTemplateSerializer(Serializer[CallTemplate]):
+ """REQUIRED
+ Serializer for call templates.
+
+ Defines the contract for serializers that convert call templates to and from
+ dictionaries for storage or transmission. Serializers are responsible for:
+ - Converting call templates to dictionaries for storage or transmission
+ - Converting dictionaries back to call templates
+ - Ensuring data consistency during serialization and deserialization
+ """
call_template_serializers: dict[str, Serializer[CallTemplate]] = {}
def to_dict(self, obj: CallTemplate) -> dict:
+ """REQUIRED
+ Convert a CallTemplate object to a dictionary.
+
+ Args:
+ obj: The CallTemplate object to convert.
+
+ Returns:
+ The dictionary converted from the CallTemplate object.
+ """
return CallTemplateSerializer.call_template_serializers[obj.call_template_type].to_dict(obj)
def validate_dict(self, obj: dict) -> CallTemplate:
+ """REQUIRED
+ Validate a dictionary and convert it to a CallTemplate object.
+
+ Args:
+ obj: The dictionary to validate and convert.
+
+ Returns:
+ The CallTemplate object converted from the dictionary.
+ """
try:
return CallTemplateSerializer.call_template_serializers[obj["call_template_type"]].validate_dict(obj)
except KeyError:
diff --git a/core/src/utcp/data/register_manual_response.py b/core/src/utcp/data/register_manual_response.py
index 756b9bd..c466032 100644
--- a/core/src/utcp/data/register_manual_response.py
+++ b/core/src/utcp/data/register_manual_response.py
@@ -4,6 +4,15 @@
from typing import List
class RegisterManualResult(BaseModel):
+ """REQUIRED
+ Result of a manual registration.
+
+ Attributes:
+ manual_call_template: The call template of the registered manual.
+ manual: The registered manual.
+ success: Whether the registration was successful.
+ errors: List of error messages if registration failed.
+ """
manual_call_template: CallTemplate
manual: UtcpManual
success: bool
diff --git a/core/src/utcp/data/tool.py b/core/src/utcp/data/tool.py
index d1262ce..effdd5c 100644
--- a/core/src/utcp/data/tool.py
+++ b/core/src/utcp/data/tool.py
@@ -21,6 +21,24 @@
JsonType = Union[str, int, float, bool, None, Dict[str, Any], List[Any]]
class JsonSchema(BaseModel):
+ """REQUIRED
+ JSON Schema for tool inputs and outputs.
+
+ Attributes:
+ schema_: Optional schema identifier.
+ id_: Optional schema identifier.
+ title: Optional schema title.
+ description: Optional schema description.
+ type: Optional schema type.
+ properties: Optional schema properties.
+ items: Optional schema items.
+ required: Optional schema required fields.
+ enum: Optional schema enum values.
+ const: Optional schema constant value.
+ default: Optional schema default value.
+ format: Optional schema format.
+ additionalProperties: Optional schema additional properties.
+ """
schema_: Optional[str] = Field(None, alias="$schema")
id_: Optional[str] = Field(None, alias="$id")
title: Optional[str] = None
@@ -50,17 +68,45 @@ class JsonSchema(BaseModel):
JsonSchema.model_rebuild() # replaces update_forward_refs()
class JsonSchemaSerializer(Serializer[JsonSchema]):
+ """REQUIRED
+ Serializer for JSON Schema.
+
+ Defines the contract for serializers that convert JSON Schema to and from
+ dictionaries for storage or transmission. Serializers are responsible for:
+ - Converting JSON Schema to dictionaries for storage or transmission
+ - Converting dictionaries back to JSON Schema
+ - Ensuring data consistency during serialization and deserialization
+ """
def to_dict(self, obj: JsonSchema) -> dict:
+ """REQUIRED
+ Convert a JsonSchema object to a dictionary.
+
+ Args:
+ obj: The JsonSchema object to convert.
+
+ Returns:
+ The dictionary converted from the JsonSchema object.
+ """
return obj.model_dump(by_alias=True)
def validate_dict(self, obj: dict) -> JsonSchema:
+ """REQUIRED
+ Validate a dictionary and convert it to a JsonSchema object.
+
+ Args:
+ obj: The dictionary to validate and convert.
+
+ Returns:
+ The JsonSchema object converted from the dictionary.
+ """
try:
return JsonSchema.model_validate(obj)
except Exception as e:
raise UtcpSerializerValidationError("Invalid JSONSchema: " + traceback.format_exc()) from e
class Tool(BaseModel):
- """Definition of a UTCP tool.
+ """REQUIRED
+ Definition of a UTCP tool.
Represents a callable tool with its metadata, input/output schemas,
and provider configuration. Tools are the fundamental units of
@@ -96,10 +142,37 @@ def validate_call_template(cls, v: Union[CallTemplate, dict]):
return CallTemplateSerializer().validate_dict(v)
class ToolSerializer(Serializer[Tool]):
+ """REQUIRED
+ Serializer for tools.
+
+ Defines the contract for serializers that convert tools to and from
+ dictionaries for storage or transmission. Serializers are responsible for:
+ - Converting tools to dictionaries for storage or transmission
+ - Converting dictionaries back to tools
+ - Ensuring data consistency during serialization and deserialization
+ """
def to_dict(self, obj: Tool) -> dict:
+ """REQUIRED
+ Convert a Tool object to a dictionary.
+
+ Args:
+ obj: The Tool object to convert.
+
+ Returns:
+ The dictionary converted from the Tool object.
+ """
return obj.model_dump(by_alias=True)
def validate_dict(self, obj: dict) -> Tool:
+ """REQUIRED
+ Validate a dictionary and convert it to a Tool object.
+
+ Args:
+ obj: The dictionary to validate and convert.
+
+ Returns:
+ The Tool object converted from the dictionary.
+ """
try:
return Tool.model_validate(obj)
except Exception as e:
diff --git a/core/src/utcp/data/utcp_client_config.py b/core/src/utcp/data/utcp_client_config.py
index 5edd9c8..7a4ce52 100644
--- a/core/src/utcp/data/utcp_client_config.py
+++ b/core/src/utcp/data/utcp_client_config.py
@@ -10,7 +10,8 @@
import traceback
class UtcpClientConfig(BaseModel):
- """Configuration model for UTCP client setup.
+ """REQUIRED
+ Configuration model for UTCP client setup.
Provides comprehensive configuration options for UTCP clients including
variable definitions, provider file locations, and variable loading
@@ -118,10 +119,37 @@ def validate_post_processing(cls, v: List[Union[ToolPostProcessor, dict]]):
return [v if isinstance(v, ToolPostProcessor) else ToolPostProcessorConfigSerializer().validate_dict(v) for v in v]
class UtcpClientConfigSerializer(Serializer[UtcpClientConfig]):
+ """REQUIRED
+ Serializer for UTCP client configurations.
+
+ Defines the contract for serializers that convert UTCP client configurations to and from
+ dictionaries for storage or transmission. Serializers are responsible for:
+ - Converting UTCP client configurations to dictionaries for storage or transmission
+ - Converting dictionaries back to UTCP client configurations
+ - Ensuring data consistency during serialization and deserialization
+ """
def to_dict(self, obj: UtcpClientConfig) -> dict:
+ """REQUIRED
+ Convert a UtcpClientConfig object to a dictionary.
+
+ Args:
+ obj: The UtcpClientConfig object to convert.
+
+ Returns:
+ The dictionary converted from the UtcpClientConfig object.
+ """
return obj.model_dump()
def validate_dict(self, data: dict) -> UtcpClientConfig:
+ """REQUIRED
+ Validate a dictionary and convert it to a UtcpClientConfig object.
+
+ Args:
+ data: The dictionary to validate and convert.
+
+ Returns:
+ The UtcpClientConfig object converted from the dictionary.
+ """
try:
return UtcpClientConfig.model_validate(data)
except Exception as e:
diff --git a/core/src/utcp/data/utcp_manual.py b/core/src/utcp/data/utcp_manual.py
index a7cfd67..562e1a6 100644
--- a/core/src/utcp/data/utcp_manual.py
+++ b/core/src/utcp/data/utcp_manual.py
@@ -18,7 +18,8 @@
import traceback
class UtcpManual(BaseModel):
- """Standard format for tool provider responses during discovery.
+ """REQUIRED
+ Standard format for tool provider responses during discovery.
Represents the complete set of tools available from a provider, along
with version information for compatibility checking. This format is
@@ -106,12 +107,31 @@ def validate_tools(cls, tools: List[Union[Tool, dict]]) -> List[Tool]:
class UtcpManualSerializer(Serializer[UtcpManual]):
- """Custom serializer for UtcpManual model."""
+ """REQUIRED
+ Serializer for UtcpManual model."""
def to_dict(self, obj: UtcpManual) -> dict:
+ """REQUIRED
+ Convert a UtcpManual object to a dictionary.
+
+ Args:
+ obj: The UtcpManual object to convert.
+
+ Returns:
+ The dictionary converted from the UtcpManual object.
+ """
return obj.model_dump()
def validate_dict(self, data: dict) -> UtcpManual:
+ """REQUIRED
+ Validate a dictionary and convert it to a UtcpManual object.
+
+ Args:
+ data: The dictionary to validate and convert.
+
+ Returns:
+ The UtcpManual object converted from the dictionary.
+ """
try:
return UtcpManual.model_validate(data)
except Exception as e:
diff --git a/core/src/utcp/data/variable_loader.py b/core/src/utcp/data/variable_loader.py
index 7dfdc3f..121ee4d 100644
--- a/core/src/utcp/data/variable_loader.py
+++ b/core/src/utcp/data/variable_loader.py
@@ -7,7 +7,8 @@
import traceback
class VariableLoader(BaseModel, ABC):
- """Abstract base class for variable loading configurations.
+ """REQUIRED
+ Abstract base class for variable loading configurations.
Defines the interface for variable loaders that can retrieve variable
values from different sources such as files, databases, or external
@@ -21,7 +22,8 @@ class VariableLoader(BaseModel, ABC):
@abstractmethod
def get(self, key: str) -> Optional[str]:
- """Retrieve a variable value by key.
+ """REQUIRED
+ Retrieve a variable value by key.
Args:
key: Variable name to retrieve.
@@ -32,13 +34,32 @@ def get(self, key: str) -> Optional[str]:
pass
class VariableLoaderSerializer(Serializer[VariableLoader]):
- """Custom serializer for VariableLoader model."""
+ """REQUIRED
+ Serializer for VariableLoader model."""
loader_serializers: Dict[str, Type[Serializer[VariableLoader]]] = {}
def to_dict(self, obj: VariableLoader) -> dict:
+ """REQUIRED
+ Convert a VariableLoader object to a dictionary.
+
+ Args:
+ obj: The VariableLoader object to convert.
+
+ Returns:
+ The dictionary converted from the VariableLoader object.
+ """
return VariableLoaderSerializer.loader_serializers[obj.variable_loader_type].to_dict(obj)
def validate_dict(self, data: dict) -> VariableLoader:
+ """REQUIRED
+ Validate a dictionary and convert it to a VariableLoader object.
+
+ Args:
+ data: The dictionary to validate and convert.
+
+ Returns:
+ The VariableLoader object converted from the dictionary.
+ """
try:
return VariableLoaderSerializer.loader_serializers[data["variable_loader_type"]].validate_dict(data)
except KeyError:
diff --git a/core/src/utcp/data/variable_loader_implementations/dot_env_variable_loader.py b/core/src/utcp/data/variable_loader_implementations/dot_env_variable_loader.py
index 2cff413..ea99e75 100644
--- a/core/src/utcp/data/variable_loader_implementations/dot_env_variable_loader.py
+++ b/core/src/utcp/data/variable_loader_implementations/dot_env_variable_loader.py
@@ -6,7 +6,8 @@
import traceback
class DotEnvVariableLoader(VariableLoader):
- """Environment file variable loader implementation.
+ """REQUIRED
+ Environment file variable loader implementation.
Loads variables from .env files using the dotenv format. This loader
supports the standard key=value format with optional quoting and
@@ -25,7 +26,8 @@ class DotEnvVariableLoader(VariableLoader):
env_file_path: str
def get(self, key: str) -> Optional[str]:
- """Load a variable from the configured .env file.
+ """REQUIRED
+ Load a variable from the configured .env file.
Args:
key: Variable name to retrieve from the environment file.
@@ -36,10 +38,30 @@ def get(self, key: str) -> Optional[str]:
return dotenv_values(self.env_file_path).get(key)
class DotEnvVariableLoaderSerializer(Serializer[DotEnvVariableLoader]):
+ """REQUIRED
+ Serializer for DotEnvVariableLoader model."""
def to_dict(self, obj: DotEnvVariableLoader) -> dict:
+ """REQUIRED
+ Convert a DotEnvVariableLoader object to a dictionary.
+
+ Args:
+ obj: The DotEnvVariableLoader object to convert.
+
+ Returns:
+ The dictionary converted from the DotEnvVariableLoader object.
+ """
return obj.model_dump()
def validate_dict(self, data: dict) -> DotEnvVariableLoader:
+ """REQUIRED
+ Validate a dictionary and convert it to a DotEnvVariableLoader object.
+
+ Args:
+ data: The dictionary to validate and convert.
+
+ Returns:
+ The DotEnvVariableLoader object converted from the dictionary.
+ """
try:
return DotEnvVariableLoader.model_validate(data)
except Exception as e:
diff --git a/core/src/utcp/exceptions/utcp_serializer_validation_error.py b/core/src/utcp/exceptions/utcp_serializer_validation_error.py
index c640408..1a935df 100644
--- a/core/src/utcp/exceptions/utcp_serializer_validation_error.py
+++ b/core/src/utcp/exceptions/utcp_serializer_validation_error.py
@@ -1,2 +1,3 @@
class UtcpSerializerValidationError(Exception):
- """Exception raised when a serializer validation fails."""
+ """REQUIRED
+ Exception raised when a serializer validation fails."""
diff --git a/core/src/utcp/exceptions/utcp_variable_not_found_exception.py b/core/src/utcp/exceptions/utcp_variable_not_found_exception.py
index 80fd2be..6a53ff0 100644
--- a/core/src/utcp/exceptions/utcp_variable_not_found_exception.py
+++ b/core/src/utcp/exceptions/utcp_variable_not_found_exception.py
@@ -1,5 +1,6 @@
class UtcpVariableNotFound(Exception):
- """Exception raised when a required variable cannot be found.
+ """REQUIRED
+ Exception raised when a required variable cannot be found.
This exception is thrown during variable substitution when a referenced
variable cannot be resolved through any of the configured variable sources.
@@ -12,7 +13,8 @@ class UtcpVariableNotFound(Exception):
variable_name: str
def __init__(self, variable_name: str):
- """Initialize the exception with the missing variable name.
+ """REQUIRED
+ Initialize the exception with the missing variable name.
Args:
variable_name: Name of the variable that could not be found.
diff --git a/core/src/utcp/implementations/default_variable_substitutor.py b/core/src/utcp/implementations/default_variable_substitutor.py
index f82932a..cb33fe4 100644
--- a/core/src/utcp/implementations/default_variable_substitutor.py
+++ b/core/src/utcp/implementations/default_variable_substitutor.py
@@ -19,7 +19,8 @@
from utcp.data.utcp_client_config import UtcpClientConfig
class DefaultVariableSubstitutor(VariableSubstitutor):
- """Default implementation of variable substitution.
+ """REQUIRED
+ Default implementation of variable substitution.
Provides a hierarchical variable resolution system that searches for
variables in the following order:
@@ -40,25 +41,6 @@ class DefaultVariableSubstitutor(VariableSubstitutor):
'web_scraper' becomes 'web__scraper_api_key' internally.
"""
def _get_variable(self, key: str, config: UtcpClientConfig, variable_namespace: Optional[str] = None) -> str:
- """Resolve a variable value through the hierarchical resolution system.
-
- Searches for the variable value in the following order:
- 1. Configuration variables dictionary
- 2. Custom variable loaders (in registration order)
- 3. Environment variables
-
- Args:
- key: Variable name to resolve.
- config: UTCP client configuration containing variable sources.
- variable_namespace: Optional variable namespace.
- When provided, the key is prefixed with the variable namespace.
-
- Returns:
- Resolved variable value as a string.
-
- Raises:
- UtcpVariableNotFound: If the variable cannot be found in any source.
- """
if variable_namespace:
key = variable_namespace.replace("_", "!").replace("!", "__") + "_" + key
if config.variables and key in config.variables:
@@ -78,7 +60,8 @@ def _get_variable(self, key: str, config: UtcpClientConfig, variable_namespace:
raise UtcpVariableNotFound(key)
def substitute(self, obj: dict | list | str, config: UtcpClientConfig, variable_namespace: Optional[str] = None) -> Any:
- """Recursively substitute variables in nested data structures.
+ """REQUIRED
+ Recursively substitute variables in nested data structures.
Performs deep substitution on dictionaries, lists, and strings.
Non-string types are returned unchanged. String values are scanned
@@ -128,7 +111,8 @@ def replacer(match):
return obj
def find_required_variables(self, obj: dict | list | str, variable_namespace: Optional[str] = None) -> List[str]:
- """Recursively discover all variable references in a data structure.
+ """REQUIRED
+ Recursively discover all variable references in a data structure.
Scans the object for variable references using ${VAR} and $VAR syntax,
returning fully-qualified variable names with variable namespacing.
diff --git a/core/src/utcp/implementations/in_mem_tool_repository.py b/core/src/utcp/implementations/in_mem_tool_repository.py
index a51642a..9082164 100644
--- a/core/src/utcp/implementations/in_mem_tool_repository.py
+++ b/core/src/utcp/implementations/in_mem_tool_repository.py
@@ -8,7 +8,8 @@
from utcp.interfaces.serializer import Serializer
class InMemToolRepository(ConcurrentToolRepository):
- """Thread-safe in-memory implementation of `ConcurrentToolRepository`.
+ """REQUIRED
+ Thread-safe in-memory implementation of `ConcurrentToolRepository`.
Stores tools and their associated manual call templates in dictionaries and
protects all operations with a read-write lock to ensure consistency under
@@ -30,6 +31,13 @@ def __init__(self):
self._manual_call_templates: Dict[str, CallTemplate] = {}
async def save_manual(self, manual_call_template: CallTemplate, manual: UtcpManual) -> None:
+ """REQUIRED
+ Save a manual and its associated tools.
+
+ Args:
+ manual_call_template: The manual call template to save.
+ manual: The manual to save.
+ """
async with self._rwlock.write():
manual_name = manual_call_template.name
@@ -48,6 +56,15 @@ async def save_manual(self, manual_call_template: CallTemplate, manual: UtcpManu
self._tools_by_name[t.name] = t
async def remove_manual(self, manual_name: str) -> bool:
+ """REQUIRED
+ Remove a manual and its associated tools.
+
+ Args:
+ manual_name: The name of the manual to remove.
+
+ Returns:
+ True if the manual was removed, False otherwise.
+ """
async with self._rwlock.write():
# Remove tools of this manual
old_manual = self._manuals.get(manual_name)
@@ -63,6 +80,15 @@ async def remove_manual(self, manual_name: str) -> bool:
return True
async def remove_tool(self, tool_name: str) -> bool:
+ """REQUIRED
+ Remove a tool from the repository.
+
+ Args:
+ tool_name: The name of the tool to remove.
+
+ Returns:
+ True if the tool was removed, False otherwise.
+ """
async with self._rwlock.write():
tool = self._tools_by_name.pop(tool_name, None)
if tool is None:
@@ -75,42 +101,119 @@ async def remove_tool(self, tool_name: str) -> bool:
return True
async def get_tool(self, tool_name: str) -> Optional[Tool]:
+ """REQUIRED
+ Get a tool by name.
+
+ Args:
+ tool_name: The name of the tool to get.
+
+ Returns:
+ The tool if it exists, None otherwise.
+ """
async with self._rwlock.read():
tool = self._tools_by_name.get(tool_name)
return tool.model_copy(deep=True) if tool else None
async def get_tools(self) -> List[Tool]:
+ """REQUIRED
+ Get all tools in the repository.
+
+ Returns:
+ A list of all tools in the repository.
+ """
async with self._rwlock.read():
return [t.model_copy(deep=True) for t in self._tools_by_name.values()]
async def get_tools_by_manual(self, manual_name: str) -> Optional[List[Tool]]:
+ """REQUIRED
+ Get all tools associated with a manual.
+
+ Args:
+ manual_name: The name of the manual to get tools for.
+
+ Returns:
+ A list of tools associated with the manual, or None if the manual does not exist.
+ """
async with self._rwlock.read():
manual = self._manuals.get(manual_name)
return [t.model_copy(deep=True) for t in manual.tools] if manual is not None else None
async def get_manual(self, manual_name: str) -> Optional[UtcpManual]:
+ """REQUIRED
+ Get a manual by name.
+
+ Args:
+ manual_name: The name of the manual to get.
+
+ Returns:
+ The manual if it exists, None otherwise.
+ """
async with self._rwlock.read():
manual = self._manuals.get(manual_name)
return manual.model_copy(deep=True) if manual else None
async def get_manuals(self) -> List[UtcpManual]:
+ """REQUIRED
+ Get all manuals in the repository.
+
+ Returns:
+ A list of all manuals in the repository.
+ """
async with self._rwlock.read():
return [m.model_copy(deep=True) for m in self._manuals.values()]
async def get_manual_call_template(self, manual_call_template_name: str) -> Optional[CallTemplate]:
+ """REQUIRED
+ Get a manual call template by name.
+
+ Args:
+ manual_call_template_name: The name of the manual call template to get.
+
+ Returns:
+ The manual call template if it exists, None otherwise.
+ """
async with self._rwlock.read():
manual_call_template = self._manual_call_templates.get(manual_call_template_name)
return manual_call_template.model_copy(deep=True) if manual_call_template else None
async def get_manual_call_templates(self) -> List[CallTemplate]:
+ """REQUIRED
+ Get all manual call templates in the repository.
+
+ Returns:
+ A list of all manual call templates in the repository.
+ """
async with self._rwlock.read():
return [m.model_copy(deep=True) for m in self._manual_call_templates.values()]
class InMemToolRepositoryConfigSerializer(Serializer[InMemToolRepository]):
+ """REQUIRED
+ Serializer for `InMemToolRepository`.
+
+ Converts an `InMemToolRepository` instance to a dictionary and vice versa.
+ """
def to_dict(self, obj: InMemToolRepository) -> dict:
+ """REQUIRED
+ Convert an `InMemToolRepository` instance to a dictionary.
+
+ Args:
+ obj: The `InMemToolRepository` instance to convert.
+
+ Returns:
+ A dictionary representing the `InMemToolRepository` instance.
+ """
return {
"tool_repository_type": obj.tool_repository_type,
}
def validate_dict(self, data: dict) -> InMemToolRepository:
+ """REQUIRED
+ Convert a dictionary to an `InMemToolRepository` instance.
+
+ Args:
+ data: The dictionary to convert.
+
+ Returns:
+ An `InMemToolRepository` instance representing the dictionary.
+ """
return InMemToolRepository()
diff --git a/core/src/utcp/implementations/tag_search.py b/core/src/utcp/implementations/tag_search.py
index 2232be5..d258c63 100644
--- a/core/src/utcp/implementations/tag_search.py
+++ b/core/src/utcp/implementations/tag_search.py
@@ -6,11 +6,28 @@
from utcp.interfaces.serializer import Serializer
class TagAndDescriptionWordMatchStrategy(ToolSearchStrategy):
+ """REQUIRED
+ Tag and description word match strategy.
+
+ This strategy matches tools based on the presence of tags and words in the description.
+ """
tool_search_strategy_type: Literal["tag_and_description_word_match"] = "tag_and_description_word_match"
description_weight: float = 1
tag_weight: float = 3
async def search_tools(self, tool_repository: ConcurrentToolRepository, query: str, limit: int = 10, any_of_tags_required: Optional[List[str]] = None) -> List[Tool]:
+ """REQUIRED
+ Search for tools based on the given query.
+
+ Args:
+ tool_repository: The tool repository to search in.
+ query: The query to search for.
+ limit: The maximum number of results to return.
+ any_of_tags_required: A list of tags that must be present in the tool.
+
+ Returns:
+ A list of tools that match the query.
+ """
if limit < 0:
raise ValueError("limit must be non-negative")
# Normalize query to lowercase and split into words
@@ -61,10 +78,33 @@ async def search_tools(self, tool_repository: ConcurrentToolRepository, query: s
return sorted_tools[:limit]
class TagAndDescriptionWordMatchStrategyConfigSerializer(Serializer[TagAndDescriptionWordMatchStrategy]):
+ """REQUIRED
+ Serializer for `TagAndDescriptionWordMatchStrategy`.
+
+ Converts a `TagAndDescriptionWordMatchStrategy` instance to a dictionary and vice versa.
+ """
def to_dict(self, obj: TagAndDescriptionWordMatchStrategy) -> dict:
+ """REQUIRED
+ Convert a `TagAndDescriptionWordMatchStrategy` instance to a dictionary.
+
+ Args:
+ obj: The `TagAndDescriptionWordMatchStrategy` instance to convert.
+
+ Returns:
+ A dictionary representing the `TagAndDescriptionWordMatchStrategy` instance.
+ """
return obj.model_dump()
def validate_dict(self, data: dict) -> TagAndDescriptionWordMatchStrategy:
+ """REQUIRED
+ Convert a dictionary to a `TagAndDescriptionWordMatchStrategy` instance.
+
+ Args:
+ data: The dictionary to convert.
+
+ Returns:
+ A `TagAndDescriptionWordMatchStrategy` instance representing the dictionary.
+ """
try:
return TagAndDescriptionWordMatchStrategy.model_validate(data)
except Exception as e:
diff --git a/core/src/utcp/implementations/utcp_client_implementation.py b/core/src/utcp/implementations/utcp_client_implementation.py
index 6a8b144..555ba2e 100644
--- a/core/src/utcp/implementations/utcp_client_implementation.py
+++ b/core/src/utcp/implementations/utcp_client_implementation.py
@@ -26,6 +26,11 @@
logger = logging.getLogger(__name__)
class UtcpClientImplementation(UtcpClient):
+ """REQUIRED
+ Implementation of the `UtcpClient` interface.
+
+ This class provides a concrete implementation of the `UtcpClient` interface.
+ """
def __init__(
self,
config: UtcpClientConfig,
@@ -41,6 +46,16 @@ async def create(
root_dir: Optional[str] = None,
config: Optional[Union[str, Dict[str, Any], UtcpClientConfig]] = None,
) -> 'UtcpClient':
+ """REQUIRED
+ Create a new `UtcpClient` instance.
+
+ Args:
+ root_dir: The root directory for the client.
+ config: The configuration for the client.
+
+ Returns:
+ A new `UtcpClient` instance.
+ """
# Validate and load the config
client_config_serializer = UtcpClientConfigSerializer()
if config is None:
@@ -77,6 +92,15 @@ async def create(
return client
async def register_manual(self, manual_call_template: CallTemplate) -> RegisterManualResult:
+ """REQUIRED
+ Register a manual in the client.
+
+ Args:
+ manual_call_template: The `CallTemplate` instance representing the manual to register.
+
+ Returns:
+ A `RegisterManualResult` instance representing the result of the registration.
+ """
# Replace all non-word characters with underscore
manual_call_template.name = re.sub(r'[^\w]', '_', manual_call_template.name)
if await self.config.tool_repository.get_manual(manual_call_template.name) is not None:
@@ -96,6 +120,15 @@ async def register_manual(self, manual_call_template: CallTemplate) -> RegisterM
return result
async def register_manuals(self, manual_call_templates: List[CallTemplate]) -> List[RegisterManualResult]:
+ """REQUIRED
+ Register multiple manuals in the client.
+
+ Args:
+ manual_call_templates: A list of `CallTemplate` instances representing the manuals to register.
+
+ Returns:
+ A list of `RegisterManualResult` instances representing the results of the registration.
+ """
# Create tasks for parallel CallTemplate registration
tasks = []
for manual_call_template in manual_call_templates:
@@ -125,6 +158,15 @@ async def try_register_manual(manual_call_template=manual_call_template):
return [p for p in results if p is not None]
async def deregister_manual(self, manual_name: str) -> bool:
+ """REQUIRED
+ Deregister a manual from the client.
+
+ Args:
+ manual_name: The name of the manual to deregister.
+
+ Returns:
+ A boolean indicating whether the manual was successfully deregistered.
+ """
manual_call_template = await self.config.tool_repository.get_manual_call_template(manual_name)
if manual_call_template is None:
return False
@@ -132,6 +174,16 @@ async def deregister_manual(self, manual_name: str) -> bool:
return await self.config.tool_repository.remove_manual(manual_name)
async def call_tool(self, tool_name: str, tool_args: Dict[str, Any]) -> Any:
+ """REQUIRED
+ Call a tool in the client.
+
+ Args:
+ tool_name: The name of the tool to call.
+ tool_args: A dictionary of arguments to pass to the tool.
+
+ Returns:
+ The result of the tool call.
+ """
manual_name = tool_name.split(".")[0]
tool = await self.config.tool_repository.get_tool(tool_name)
if tool is None:
@@ -145,6 +197,16 @@ async def call_tool(self, tool_name: str, tool_args: Dict[str, Any]) -> Any:
return result
async def call_tool_streaming(self, tool_name: str, tool_args: Dict[str, Any]) -> AsyncGenerator[Any, None]:
+ """REQUIRED
+ Call a tool in the client streamingly.
+
+ Args:
+ tool_name: The name of the tool to call.
+ tool_args: A dictionary of arguments to pass to the tool.
+
+ Returns:
+ An async generator yielding the result of the tool call.
+ """
manual_name = tool_name.split(".")[0]
tool = await self.config.tool_repository.get_tool(tool_name)
if tool is None:
@@ -157,6 +219,17 @@ async def call_tool_streaming(self, tool_name: str, tool_args: Dict[str, Any]) -
yield item
async def search_tools(self, query: str, limit: int = 10, any_of_tags_required: Optional[List[str]] = None) -> List[Tool]:
+ """REQUIRED
+ Search for tools based on the given query.
+
+ Args:
+ query: The query to search for.
+ limit: The maximum number of results to return.
+ any_of_tags_required: A list of tags that must be present in the tool.
+
+ Returns:
+ A list of tools that match the query.
+ """
return await self.config.tool_search_strategy.search_tools(
tool_repository=self.config.tool_repository,
query=query,
@@ -165,6 +238,15 @@ async def search_tools(self, query: str, limit: int = 10, any_of_tags_required:
)
async def get_required_variables_for_manual_and_tools(self, manual_call_template: CallTemplate) -> List[str]:
+ """REQUIRED
+ Get the required variables for a manual and its tools.
+
+ Args:
+ manual_call_template: The `CallTemplate` instance representing the manual.
+
+ Returns:
+ A list of required variables for the manual and its tools.
+ """
manual_call_template.name = re.sub(r'[^\w]', '_', manual_call_template.name)
variables_for_CallTemplate = self.variable_substitutor.find_required_variables(CallTemplateSerializer().to_dict(manual_call_template), manual_call_template.name)
if len(variables_for_CallTemplate) > 0:
@@ -181,6 +263,15 @@ async def get_required_variables_for_manual_and_tools(self, manual_call_template
return variables_for_CallTemplate
async def get_required_variables_for_registered_tool(self, tool_name: str) -> List[str]:
+ """REQUIRED
+ Get the required variables for a registered tool.
+
+ Args:
+ tool_name: The name of the tool.
+
+ Returns:
+ A list of required variables for the tool.
+ """
manual_name = tool_name.split(".")[0]
tool = await self.config.tool_repository.get_tool(tool_name)
if tool is None:
diff --git a/core/src/utcp/interfaces/communication_protocol.py b/core/src/utcp/interfaces/communication_protocol.py
index b3c31a1..b12e6ea 100644
--- a/core/src/utcp/interfaces/communication_protocol.py
+++ b/core/src/utcp/interfaces/communication_protocol.py
@@ -13,7 +13,8 @@
from utcp.utcp_client import UtcpClient
class CommunicationProtocol(ABC):
- """Abstract interface for UTCP client transport implementations.
+ """REQUIRED
+ Abstract interface for UTCP client transport implementations.
Defines the contract that all transport implementations must follow to
integrate with the UTCP client. Each transport handles communication
@@ -28,7 +29,8 @@ class CommunicationProtocol(ABC):
@abstractmethod
async def register_manual(self, caller: 'UtcpClient', manual_call_template: CallTemplate) -> RegisterManualResult:
- """Register a manual and its tools.
+ """REQUIRED
+ Register a manual and its tools.
Connects to the provider and retrieves the list of tools it offers.
This may involve making discovery requests, parsing configuration files,
@@ -49,7 +51,8 @@ async def register_manual(self, caller: 'UtcpClient', manual_call_template: Call
@abstractmethod
async def deregister_manual(self, caller: 'UtcpClient', manual_call_template: CallTemplate) -> None:
- """Deregister a manual and its tools.
+ """REQUIRED
+ Deregister a manual and its tools.
Cleanly disconnects from the provider and releases any associated
resources such as connections, processes, or file handles.
@@ -66,7 +69,8 @@ async def deregister_manual(self, caller: 'UtcpClient', manual_call_template: Ca
@abstractmethod
async def call_tool(self, caller: 'UtcpClient', tool_name: str, tool_args: Dict[str, Any], tool_call_template: CallTemplate) -> Any:
- """Execute a tool call through this transport.
+ """REQUIRED
+ Execute a tool call through this transport.
Sends a tool invocation request to the provider using the appropriate
protocol and returns the result. Handles serialization of arguments
@@ -91,7 +95,8 @@ async def call_tool(self, caller: 'UtcpClient', tool_name: str, tool_args: Dict[
@abstractmethod
async def call_tool_streaming(self, caller: 'UtcpClient', tool_name: str, tool_args: Dict[str, Any], tool_call_template: CallTemplate) -> AsyncGenerator[Any, None]:
- """Execute a tool call through this transport streamingly.
+ """REQUIRED
+ Execute a tool call through this transport streamingly.
Sends a tool invocation request to the provider using the appropriate
protocol and returns the result. Handles serialization of arguments
diff --git a/core/src/utcp/interfaces/concurrent_tool_repository.py b/core/src/utcp/interfaces/concurrent_tool_repository.py
index fea47f2..6ab2564 100644
--- a/core/src/utcp/interfaces/concurrent_tool_repository.py
+++ b/core/src/utcp/interfaces/concurrent_tool_repository.py
@@ -18,7 +18,8 @@
import traceback
class ConcurrentToolRepository(BaseModel, ABC):
- """Abstract interface for tool and provider storage implementations.
+ """REQUIRED
+ Abstract interface for tool and provider storage implementations.
Defines the contract for repositories that manage the lifecycle and storage
of UTCP tools and call templates. Repositories are responsible for:
@@ -40,7 +41,7 @@ class ConcurrentToolRepository(BaseModel, ABC):
@abstractmethod
async def save_manual(self, manual_call_template: CallTemplate, manual: UtcpManual) -> None:
- """
+ """REQUIRED
Save a manual and its tools in the repository.
Args:
@@ -51,7 +52,7 @@ async def save_manual(self, manual_call_template: CallTemplate, manual: UtcpManu
@abstractmethod
async def remove_manual(self, manual_name: str) -> bool:
- """
+ """REQUIRED
Remove a manual and its tools from the repository.
Args:
@@ -64,7 +65,7 @@ async def remove_manual(self, manual_name: str) -> bool:
@abstractmethod
async def remove_tool(self, tool_name: str) -> bool:
- """
+ """REQUIRED
Remove a tool from the repository.
Args:
@@ -77,7 +78,7 @@ async def remove_tool(self, tool_name: str) -> bool:
@abstractmethod
async def get_tool(self, tool_name: str) -> Optional[Tool]:
- """
+ """REQUIRED
Get a tool from the repository.
Args:
@@ -90,7 +91,7 @@ async def get_tool(self, tool_name: str) -> Optional[Tool]:
@abstractmethod
async def get_tools(self) -> List[Tool]:
- """
+ """REQUIRED
Get all tools from the repository.
Returns:
@@ -100,7 +101,7 @@ async def get_tools(self) -> List[Tool]:
@abstractmethod
async def get_tools_by_manual(self, manual_name: str) -> Optional[List[Tool]]:
- """
+ """REQUIRED
Get tools associated with a specific manual.
Args:
@@ -113,7 +114,7 @@ async def get_tools_by_manual(self, manual_name: str) -> Optional[List[Tool]]:
@abstractmethod
async def get_manual(self, manual_name: str) -> Optional[UtcpManual]:
- """
+ """REQUIRED
Get a manual from the repository.
Args:
@@ -126,7 +127,7 @@ async def get_manual(self, manual_name: str) -> Optional[UtcpManual]:
@abstractmethod
async def get_manuals(self) -> List[UtcpManual]:
- """
+ """REQUIRED
Get all manuals from the repository.
Returns:
@@ -136,7 +137,7 @@ async def get_manuals(self) -> List[UtcpManual]:
@abstractmethod
async def get_manual_call_template(self, manual_call_template_name: str) -> Optional[CallTemplate]:
- """
+ """REQUIRED
Get a manual call template from the repository.
Args:
@@ -149,7 +150,7 @@ async def get_manual_call_template(self, manual_call_template_name: str) -> Opti
@abstractmethod
async def get_manual_call_templates(self) -> List[CallTemplate]:
- """
+ """REQUIRED
Get all manual call templates from the repository.
Returns:
diff --git a/core/src/utcp/interfaces/serializer.py b/core/src/utcp/interfaces/serializer.py
index a634a7b..98ee2f7 100644
--- a/core/src/utcp/interfaces/serializer.py
+++ b/core/src/utcp/interfaces/serializer.py
@@ -5,17 +5,53 @@
T = TypeVar('T')
class Serializer(ABC, Generic[T]):
+ """REQUIRED
+ Abstract interface for serializers.
+
+ Defines the contract for serializers that convert objects to and from
+ dictionaries for storage or transmission. Serializers are responsible for:
+ - Converting objects to dictionaries for storage or transmission
+ - Converting dictionaries back to objects
+ - Ensuring data consistency during serialization and deserialization
+ """
def __init__(self):
ensure_plugins_initialized()
@abstractmethod
def validate_dict(self, obj: dict) -> T:
+ """REQUIRED
+ Validate a dictionary and convert it to an object.
+
+ Args:
+ obj: The dictionary to validate and convert.
+
+ Returns:
+ The object converted from the dictionary.
+ """
pass
@abstractmethod
def to_dict(self, obj: T) -> dict:
+ """REQUIRED
+ Convert an object to a dictionary.
+
+ Args:
+ obj: The object to convert.
+
+ Returns:
+ The dictionary converted from the object.
+ """
pass
def copy(self, obj: T) -> T:
+ """REQUIRED
+ Create a copy of an object.
+
+ Args:
+ obj: The object to copy.
+
+ Returns:
+ A copy of the object.
+ """
return self.validate_dict(self.to_dict(obj))
diff --git a/core/src/utcp/interfaces/tool_post_processor.py b/core/src/utcp/interfaces/tool_post_processor.py
index 257c175..d31d840 100644
--- a/core/src/utcp/interfaces/tool_post_processor.py
+++ b/core/src/utcp/interfaces/tool_post_processor.py
@@ -9,19 +9,66 @@
import traceback
class ToolPostProcessor(BaseModel, ABC):
+ """REQUIRED
+ Abstract interface for tool post processors.
+
+ Defines the contract for tool post processors that process the result of a tool call.
+ Tool post processors are responsible for:
+ - Processing the result of a tool call
+ - Returning the processed result
+ """
tool_post_processor_type: str
@abstractmethod
def post_process(self, caller: 'UtcpClient', tool: Tool, manual_call_template: 'CallTemplate', result: Any) -> Any:
+ """REQUIRED
+ Process the result of a tool call.
+
+ Args:
+ caller: The UTCP client that is calling this method.
+ tool: The tool that was called.
+ manual_call_template: The call template of the manual that was called.
+ result: The result of the tool call.
+
+ Returns:
+ The processed result.
+ """
raise NotImplementedError
class ToolPostProcessorConfigSerializer(Serializer[ToolPostProcessor]):
+ """REQUIRED
+ Serializer for tool post processors.
+
+ Defines the contract for serializers that convert tool post processors to and from
+ dictionaries for storage or transmission. Serializers are responsible for:
+ - Converting tool post processors to dictionaries for storage or transmission
+ - Converting dictionaries back to tool post processors
+ - Ensuring data consistency during serialization and deserialization
+ """
tool_post_processor_implementations: Dict[str, Serializer[ToolPostProcessor]] = {}
def to_dict(self, obj: ToolPostProcessor) -> dict:
+ """REQUIRED
+ Convert a tool post processor to a dictionary.
+
+ Args:
+ obj: The tool post processor to convert.
+
+ Returns:
+ The dictionary converted from the tool post processor.
+ """
return ToolPostProcessorConfigSerializer.tool_post_processor_implementations[obj.tool_post_processor_type].to_dict(obj)
def validate_dict(self, data: dict) -> ToolPostProcessor:
+ """REQUIRED
+ Validate a dictionary and convert it to a tool post processor.
+
+ Args:
+ data: The dictionary to validate and convert.
+
+ Returns:
+ The tool post processor converted from the dictionary.
+ """
try:
return ToolPostProcessorConfigSerializer.tool_post_processor_implementations[data['tool_post_processor_type']].validate_dict(data)
except KeyError:
diff --git a/core/src/utcp/interfaces/tool_search_strategy.py b/core/src/utcp/interfaces/tool_search_strategy.py
index bc6e93d..449b372 100644
--- a/core/src/utcp/interfaces/tool_search_strategy.py
+++ b/core/src/utcp/interfaces/tool_search_strategy.py
@@ -15,7 +15,8 @@
import traceback
class ToolSearchStrategy(BaseModel, ABC):
- """Abstract interface for tool search implementations.
+ """REQUIRED
+ Abstract interface for tool search implementations.
Defines the contract for tool search strategies that can be plugged into
the UTCP client. Different implementations can provide various search
@@ -32,7 +33,8 @@ class ToolSearchStrategy(BaseModel, ABC):
@abstractmethod
async def search_tools(self, tool_repository: ConcurrentToolRepository, query: str, limit: int = 10, any_of_tags_required: Optional[List[str]] = None) -> List[Tool]:
- """Search for tools relevant to the query.
+ """REQUIRED
+ Search for tools relevant to the query.
Executes a search against the available tools and returns the most
relevant matches ranked by the strategy's scoring algorithm.
@@ -57,13 +59,40 @@ async def search_tools(self, tool_repository: ConcurrentToolRepository, query: s
pass
class ToolSearchStrategyConfigSerializer(Serializer[ToolSearchStrategy]):
+ """REQUIRED
+ Serializer for tool search strategies.
+
+ Defines the contract for serializers that convert tool search strategies to and from
+ dictionaries for storage or transmission. Serializers are responsible for:
+ - Converting tool search strategies to dictionaries for storage or transmission
+ - Converting dictionaries back to tool search strategies
+ - Ensuring data consistency during serialization and deserialization
+ """
tool_search_strategy_implementations: Dict[str, Serializer['ToolSearchStrategy']] = {}
default_strategy = "tag_and_description_word_match"
def to_dict(self, obj: ToolSearchStrategy) -> dict:
+ """REQUIRED
+ Convert a tool search strategy to a dictionary.
+
+ Args:
+ obj: The tool search strategy to convert.
+
+ Returns:
+ The dictionary converted from the tool search strategy.
+ """
return ToolSearchStrategyConfigSerializer.tool_search_strategy_implementations[obj.tool_search_strategy_type].to_dict(obj)
def validate_dict(self, data: dict) -> ToolSearchStrategy:
+ """REQUIRED
+ Validate a dictionary and convert it to a tool search strategy.
+
+ Args:
+ data: The dictionary to validate and convert.
+
+ Returns:
+ The tool search strategy converted from the dictionary.
+ """
try:
return ToolSearchStrategyConfigSerializer.tool_search_strategy_implementations[data['tool_search_strategy_type']].validate_dict(data)
except KeyError:
diff --git a/core/src/utcp/interfaces/variable_substitutor.py b/core/src/utcp/interfaces/variable_substitutor.py
index 8301044..1a46418 100644
--- a/core/src/utcp/interfaces/variable_substitutor.py
+++ b/core/src/utcp/interfaces/variable_substitutor.py
@@ -3,7 +3,8 @@
from utcp.data.utcp_client_config import UtcpClientConfig
class VariableSubstitutor(ABC):
- """Abstract interface for variable substitution implementations.
+ """REQUIRED
+ Abstract interface for variable substitution implementations.
Defines the contract for variable substitution systems that can replace
placeholders in configuration data with actual values from various sources.
@@ -13,7 +14,8 @@ class VariableSubstitutor(ABC):
@abstractmethod
def substitute(self, obj: dict | list | str, config: UtcpClientConfig, variable_namespace: Optional[str] = None) -> Any:
- """Substitute variables in the given object.
+ """REQUIRED
+ Substitute variables in the given object.
Args:
obj: Object containing potential variable references to substitute.
@@ -31,7 +33,8 @@ def substitute(self, obj: dict | list | str, config: UtcpClientConfig, variable_
@abstractmethod
def find_required_variables(self, obj: dict | list | str, variable_namespace: Optional[str] = None) -> List[str]:
- """Find all variable references in the given object.
+ """REQUIRED
+ Find all variable references in the given object.
Args:
obj: Object to scan for variable references.
diff --git a/core/src/utcp/plugins/discovery.py b/core/src/utcp/plugins/discovery.py
index 7cc7918..830214e 100644
--- a/core/src/utcp/plugins/discovery.py
+++ b/core/src/utcp/plugins/discovery.py
@@ -11,6 +11,17 @@
logger = logging.getLogger(__name__)
def register_auth(auth_type: str, serializer: Serializer[Auth], override: bool = False) -> bool:
+ """REQUIRED
+ Register an authentication implementation.
+
+ Args:
+ auth_type: The authentication type identifier.
+ serializer: The serializer for the authentication implementation.
+ override: Whether to override an existing implementation.
+
+ Returns:
+ True if the implementation was registered, False otherwise.
+ """
if not override and auth_type in AuthSerializer.auth_serializers:
return False
AuthSerializer.auth_serializers[auth_type] = serializer
@@ -18,6 +29,17 @@ def register_auth(auth_type: str, serializer: Serializer[Auth], override: bool =
return True
def register_variable_loader(loader_type: str, serializer: Serializer[VariableLoader], override: bool = False) -> bool:
+ """REQUIRED
+ Register a variable loader implementation.
+
+ Args:
+ loader_type: The variable loader type identifier.
+ serializer: The serializer for the variable loader implementation.
+ override: Whether to override an existing implementation.
+
+ Returns:
+ True if the implementation was registered, False otherwise.
+ """
if not override and loader_type in VariableLoaderSerializer.loader_serializers:
return False
VariableLoaderSerializer.loader_serializers[loader_type] = serializer
@@ -25,6 +47,17 @@ def register_variable_loader(loader_type: str, serializer: Serializer[VariableLo
return True
def register_call_template(call_template_type: str, serializer: Serializer[CallTemplate], override: bool = False) -> bool:
+ """REQUIRED
+ Register a call template implementation.
+
+ Args:
+ call_template_type: The call template type identifier.
+ serializer: The serializer for the call template implementation.
+ override: Whether to override an existing implementation.
+
+ Returns:
+ True if the implementation was registered, False otherwise.
+ """
if not override and call_template_type in CallTemplateSerializer.call_template_serializers:
return False
CallTemplateSerializer.call_template_serializers[call_template_type] = serializer
@@ -32,6 +65,17 @@ def register_call_template(call_template_type: str, serializer: Serializer[CallT
return True
def register_communication_protocol(communication_protocol_type: str, communication_protocol: CommunicationProtocol, override: bool = False) -> bool:
+ """REQUIRED
+ Register a communication protocol implementation.
+
+ Args:
+ communication_protocol_type: The communication protocol type identifier.
+ communication_protocol: The communication protocol implementation.
+ override: Whether to override an existing implementation.
+
+ Returns:
+ True if the implementation was registered, False otherwise.
+ """
if not override and communication_protocol_type in CommunicationProtocol.communication_protocols:
return False
CommunicationProtocol.communication_protocols[communication_protocol_type] = communication_protocol
@@ -39,6 +83,17 @@ def register_communication_protocol(communication_protocol_type: str, communicat
return True
def register_tool_repository(tool_repository_type: str, tool_repository: Serializer[ConcurrentToolRepository], override: bool = False) -> bool:
+ """REQUIRED
+ Register a tool repository implementation.
+
+ Args:
+ tool_repository_type: The tool repository type identifier.
+ tool_repository: The tool repository implementation.
+ override: Whether to override an existing implementation.
+
+ Returns:
+ True if the implementation was registered, False otherwise.
+ """
if not override and tool_repository_type in ConcurrentToolRepositoryConfigSerializer.tool_repository_implementations:
return False
ConcurrentToolRepositoryConfigSerializer.tool_repository_implementations[tool_repository_type] = tool_repository
@@ -46,6 +101,17 @@ def register_tool_repository(tool_repository_type: str, tool_repository: Seriali
return True
def register_tool_search_strategy(strategy_type: str, strategy: Serializer[ToolSearchStrategy], override: bool = False) -> bool:
+ """REQUIRED
+ Register a tool search strategy implementation.
+
+ Args:
+ strategy_type: The tool search strategy type identifier.
+ strategy: The tool search strategy implementation.
+ override: Whether to override an existing implementation.
+
+ Returns:
+ True if the implementation was registered, False otherwise.
+ """
if not override and strategy_type in ToolSearchStrategyConfigSerializer.tool_search_strategy_implementations:
return False
ToolSearchStrategyConfigSerializer.tool_search_strategy_implementations[strategy_type] = strategy
@@ -53,6 +119,17 @@ def register_tool_search_strategy(strategy_type: str, strategy: Serializer[ToolS
return True
def register_tool_post_processor(tool_post_processor_type: str, tool_post_processor: Serializer[ToolPostProcessor], override: bool = False) -> bool:
+ """REQUIRED
+ Register a tool post processor implementation.
+
+ Args:
+ tool_post_processor_type: The tool post processor type identifier.
+ tool_post_processor: The tool post processor implementation.
+ override: Whether to override an existing implementation.
+
+ Returns:
+ True if the implementation was registered, False otherwise.
+ """
if not override and tool_post_processor_type in ToolPostProcessorConfigSerializer.tool_post_processor_implementations:
return False
ToolPostProcessorConfigSerializer.tool_post_processor_implementations[tool_post_processor_type] = tool_post_processor
diff --git a/core/src/utcp/plugins/plugin_loader.py b/core/src/utcp/plugins/plugin_loader.py
index 6fa2cf6..18b6b0b 100644
--- a/core/src/utcp/plugins/plugin_loader.py
+++ b/core/src/utcp/plugins/plugin_loader.py
@@ -31,6 +31,11 @@ def _load_plugins():
loading_plugins = False
def ensure_plugins_initialized():
+ """REQUIRED
+ Ensure that plugins are initialized.
+
+ This function should be called before using any plugin related functionality is used.
+ """
global plugins_initialized
global loading_plugins
if plugins_initialized:
diff --git a/core/src/utcp/utcp_client.py b/core/src/utcp/utcp_client.py
index 69cd499..6dd2172 100644
--- a/core/src/utcp/utcp_client.py
+++ b/core/src/utcp/utcp_client.py
@@ -1,17 +1,3 @@
-"""Main UTCP client implementation.
-
-This module provides the primary client interface for the Universal Tool Calling
-Protocol. The UtcpClient class manages multiple transport implementations,
-tool repositories, search strategies, and CallTemplate configurations.
-
-Key Features:
- - Multi-transport support (HTTP, CLI, WebSocket, etc.)
- - Dynamic CallTemplate registration and deregistration
- - Tool discovery and search capabilities
- - Variable substitution for configuration
- - Pluggable tool repositories and search strategies
-"""
-
from abc import ABC, abstractmethod
from typing import Dict, Any, List, Union, Optional, AsyncGenerator, TYPE_CHECKING
@@ -24,17 +10,12 @@
from utcp.data.utcp_client_config import UtcpClientConfig
class UtcpClient(ABC):
- """Abstract interface for UTCP client implementations.
+ """REQUIRED
+ Abstract interface for UTCP client implementations.
Defines the core contract for UTCP clients, including CallTemplate management,
tool execution, search capabilities, and variable handling. This interface
allows for different client implementations while maintaining consistency.
-
- The interface supports:
- - CallTemplate lifecycle management (register/deregister)
- - Tool discovery and execution
- - Tool search and filtering
- - Configuration variable validation
"""
def __init__(
@@ -51,7 +32,7 @@ async def create(
root_dir: Optional[str] = None,
config: Optional[Union[str, Dict[str, Any], 'UtcpClientConfig']] = None,
) -> 'UtcpClient':
- """
+ """REQUIRED
Create a new instance of UtcpClient.
Args:
@@ -72,7 +53,7 @@ async def create(
@abstractmethod
async def register_manual(self, manual_call_template: CallTemplate) -> RegisterManualResult:
- """
+ """REQUIRED
Register a tool CallTemplate and its tools.
Args:
@@ -85,7 +66,7 @@ async def register_manual(self, manual_call_template: CallTemplate) -> RegisterM
@abstractmethod
async def register_manuals(self, manual_call_templates: List[CallTemplate]) -> List[RegisterManualResult]:
- """
+ """REQUIRED
Register multiple tool CallTemplates and their tools.
Args:
@@ -98,7 +79,7 @@ async def register_manuals(self, manual_call_templates: List[CallTemplate]) -> L
@abstractmethod
async def deregister_manual(self, manual_call_template_name: str) -> bool:
- """
+ """REQUIRED
Deregister a tool CallTemplate.
Args:
@@ -111,7 +92,7 @@ async def deregister_manual(self, manual_call_template_name: str) -> bool:
@abstractmethod
async def call_tool(self, tool_name: str, tool_args: Dict[str, Any]) -> Any:
- """
+ """REQUIRED
Call a tool.
Args:
@@ -125,7 +106,7 @@ async def call_tool(self, tool_name: str, tool_args: Dict[str, Any]) -> Any:
@abstractmethod
async def call_tool_streaming(self, tool_name: str, tool_args: Dict[str, Any]) -> AsyncGenerator[Any, None]:
- """
+ """REQUIRED
Call a tool streamingly.
Args:
@@ -139,7 +120,7 @@ async def call_tool_streaming(self, tool_name: str, tool_args: Dict[str, Any]) -
@abstractmethod
async def search_tools(self, query: str, limit: int = 10, any_of_tags_required: Optional[List[str]] = None) -> List[Tool]:
- """
+ """REQUIRED
Search for tools relevant to the query.
Args:
@@ -154,7 +135,7 @@ async def search_tools(self, query: str, limit: int = 10, any_of_tags_required:
@abstractmethod
async def get_required_variables_for_manual_and_tools(self, manual_call_template: CallTemplate) -> List[str]:
- """
+ """REQUIRED
Get the required variables for a manual CallTemplate and its tools.
Args:
@@ -167,7 +148,7 @@ async def get_required_variables_for_manual_and_tools(self, manual_call_template
@abstractmethod
async def get_required_variables_for_registered_tool(self, tool_name: str) -> List[str]:
- """
+ """REQUIRED
Get the required variables for a registered tool.
Args:
diff --git a/plugins/communication_protocols/cli/src/utcp_cli/cli_call_template.py b/plugins/communication_protocols/cli/src/utcp_cli/cli_call_template.py
index 60ac21f..3d83508 100644
--- a/plugins/communication_protocols/cli/src/utcp_cli/cli_call_template.py
+++ b/plugins/communication_protocols/cli/src/utcp_cli/cli_call_template.py
@@ -7,7 +7,8 @@
import traceback
class CliCallTemplate(CallTemplate):
- """Call template configuration for Command Line Interface tools.
+ """REQUIRED
+ Call template configuration for Command Line Interface tools.
Enables execution of command-line tools and programs as UTCP providers.
Supports environment variable injection and custom working directories.
@@ -32,12 +33,17 @@ class CliCallTemplate(CallTemplate):
class CliCallTemplateSerializer(Serializer[CliCallTemplate]):
- """Serializer for CliCallTemplate."""
+ """REQUIRED
+ Serializer for CliCallTemplate."""
def to_dict(self, obj: CliCallTemplate) -> dict:
+ """REQUIRED
+ Converts a CliCallTemplate to a dictionary."""
return obj.model_dump()
def validate_dict(self, obj: dict) -> CliCallTemplate:
+ """REQUIRED
+ Validates a dictionary and returns a CliCallTemplate."""
try:
return CliCallTemplate.model_validate(obj)
except Exception as e:
diff --git a/plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py b/plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py
index b0293d0..148e261 100644
--- a/plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py
+++ b/plugins/communication_protocols/cli/src/utcp_cli/cli_communication_protocol.py
@@ -37,7 +37,8 @@
class CliCommunicationProtocol(CommunicationProtocol):
- """Transport implementation for CLI-based tool providers.
+ """REQUIRED
+ Transport implementation for CLI-based tool providers.
Handles communication with command-line tools by executing processes
and managing their input/output. Supports both tool discovery and
@@ -154,7 +155,8 @@ async def _execute_command(
raise
async def register_manual(self, caller, manual_call_template: CallTemplate) -> RegisterManualResult:
- """Register a CLI manual and discover its tools.
+ """REQUIRED
+ Register a CLI manual and discover its tools.
Executes the call template's command_name and looks for a UTCP manual JSON in the output.
"""
@@ -237,7 +239,8 @@ async def register_manual(self, caller, manual_call_template: CallTemplate) -> R
)
async def deregister_manual(self, caller, manual_call_template: CallTemplate) -> None:
- """Deregister a CLI manual (no-op)."""
+ """REQUIRED
+ Deregister a CLI manual (no-op)."""
if isinstance(manual_call_template, CliCallTemplate):
self._log_info(
f"Deregistering CLI manual '{manual_call_template.name}' (no-op)"
@@ -399,7 +402,8 @@ def _parse_tool_data(self, data: Any, provider_name: str) -> List[Tool]:
return tools
async def call_tool(self, caller, tool_name: str, tool_args: Dict[str, Any], tool_call_template: CallTemplate) -> Any:
- """Call a CLI tool.
+ """REQUIRED
+ Call a CLI tool.
Executes the command specified by provider.command_name with the provided arguments.
@@ -471,11 +475,13 @@ async def call_tool(self, caller, tool_name: str, tool_args: Dict[str, Any], too
raise
async def call_tool_streaming(self, caller, tool_name: str, tool_args: Dict[str, Any], tool_call_template: CallTemplate) -> AsyncGenerator[Any, None]:
- """Streaming calls are not supported for CLI protocol."""
+ """REQUIRED
+ Streaming calls are not supported for CLI protocol."""
raise NotImplementedError("Streaming is not supported by the CLI communication protocol.")
async def close(self) -> None:
- """Close the transport.
+ """
+ Close the transport.
This is a no-op for CLI transports since they don't maintain connections.
"""
diff --git a/plugins/communication_protocols/http/src/utcp_http/http_call_template.py b/plugins/communication_protocols/http/src/utcp_http/http_call_template.py
index c422aeb..73e4d64 100644
--- a/plugins/communication_protocols/http/src/utcp_http/http_call_template.py
+++ b/plugins/communication_protocols/http/src/utcp_http/http_call_template.py
@@ -7,7 +7,8 @@
from pydantic import Field
class HttpCallTemplate(CallTemplate):
- """Provider configuration for HTTP-based tools.
+ """REQUIRED
+ Provider configuration for HTTP-based tools.
Supports RESTful HTTP/HTTPS APIs with various HTTP methods, authentication,
custom headers, and flexible request/response handling. Supports URL path
@@ -37,12 +38,17 @@ class HttpCallTemplate(CallTemplate):
class HttpCallTemplateSerializer(Serializer[HttpCallTemplate]):
- """Serializer for HttpCallTemplate."""
+ """REQUIRED
+ Serializer for HttpCallTemplate."""
def to_dict(self, obj: HttpCallTemplate) -> dict:
+ """REQUIRED
+ Convert HttpCallTemplate to dictionary."""
return obj.model_dump()
def validate_dict(self, obj: dict) -> HttpCallTemplate:
+ """REQUIRED
+ Validate dictionary and convert to HttpCallTemplate."""
try:
return HttpCallTemplate.model_validate(obj)
except Exception as e:
diff --git a/plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py b/plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py
index 94a3b4b..97fa96b 100644
--- a/plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py
+++ b/plugins/communication_protocols/http/src/utcp_http/http_communication_protocol.py
@@ -37,7 +37,8 @@
logger = logging.getLogger(__name__)
class HttpCommunicationProtocol(CommunicationProtocol):
- """HTTP communication protocol implementation for UTCP client.
+ """REQUIRED
+ HTTP communication protocol implementation for UTCP client.
Handles communication with HTTP-based tool providers, supporting various
authentication methods, URL path parameters, and automatic tool discovery.
@@ -101,7 +102,8 @@ def _apply_auth(self, provider: HttpCallTemplate, headers: Dict[str, str], query
return auth, cookies
async def register_manual(self, caller, manual_call_template: CallTemplate) -> RegisterManualResult:
- """Register a manual and its tools.
+ """REQUIRED
+ Register a manual and its tools.
Args:
caller: The UTCP client that is calling this method.
@@ -227,14 +229,16 @@ async def register_manual(self, caller, manual_call_template: CallTemplate) -> R
)
async def deregister_manual(self, caller, manual_call_template: CallTemplate) -> None:
- """Deregister a manual and its tools.
+ """REQUIRED
+ Deregister a manual and its tools.
Deregistering a manual is a no-op for the stateless HTTP communication protocol.
"""
pass
async def call_tool(self, caller, tool_name: str, tool_args: Dict[str, Any], tool_call_template: CallTemplate) -> Any:
- """Execute a tool call through this transport.
+ """REQUIRED
+ Execute a tool call through this transport.
Args:
caller: The UTCP client that is calling this method.
@@ -316,7 +320,8 @@ async def call_tool(self, caller, tool_name: str, tool_args: Dict[str, Any], too
raise
async def call_tool_streaming(self, caller, tool_name: str, tool_args: Dict[str, Any], tool_call_template: CallTemplate) -> AsyncGenerator[Any, None]:
- """Execute a tool call through this transport streamingly.
+ """REQUIRED
+ Execute a tool call through this transport streamingly.
Args:
caller: The UTCP client that is calling this method.
@@ -332,7 +337,8 @@ async def call_tool_streaming(self, caller, tool_name: str, tool_args: Dict[str,
yield result
async def _handle_oauth2(self, auth_details: OAuth2Auth) -> str:
- """Handles OAuth2 client credentials flow, trying both body and auth header methods."""
+ """
+ Handles OAuth2 client credentials flow, trying both body and auth header methods."""
client_id = auth_details.client_id
if client_id in self._oauth_tokens:
@@ -373,7 +379,8 @@ async def _handle_oauth2(self, auth_details: OAuth2Auth) -> str:
logger.error(f"OAuth2 with Basic Auth header also failed: {e}")
def _build_url_with_path_params(self, url_template: str, tool_args: Dict[str, Any]) -> str:
- """Build URL by substituting path parameters from arguments.
+ """
+ Build URL by substituting path parameters from arguments.
Args:
url_template: URL template with path parameters in {param_name} format
diff --git a/plugins/communication_protocols/http/src/utcp_http/openapi_converter.py b/plugins/communication_protocols/http/src/utcp_http/openapi_converter.py
index 676ddf8..1a6b679 100644
--- a/plugins/communication_protocols/http/src/utcp_http/openapi_converter.py
+++ b/plugins/communication_protocols/http/src/utcp_http/openapi_converter.py
@@ -29,7 +29,8 @@
from utcp_http.http_call_template import HttpCallTemplate
class OpenApiConverter:
- """Converts OpenAPI specifications into UTCP tool definitions.
+ """REQUIRED
+ Converts OpenAPI specifications into UTCP tool definitions.
Processes OpenAPI 2.0 and 3.0 specifications to generate equivalent UTCP
tools, handling schema resolution, authentication mapping, and proper
@@ -97,7 +98,8 @@ def _get_placeholder(self, placeholder_name: str) -> str:
return f"${{{placeholder_name}_{self.placeholder_counter}}}"
def convert(self) -> UtcpManual:
- """Parses the OpenAPI specification and returns a UtcpManual."""
+ """REQUIRED
+ Parses the OpenAPI specification and returns a UtcpManual."""
self.placeholder_counter = 0
tools = []
servers = self.spec.get("servers")
@@ -121,7 +123,8 @@ def convert(self) -> UtcpManual:
return UtcpManual(tools=tools)
def _extract_auth(self, operation: Dict[str, Any]) -> Optional[Auth]:
- """Extracts authentication information from OpenAPI operation and global security schemes."""
+ """
+ Extracts authentication information from OpenAPI operation and global security schemes."""
# First check for operation-level security requirements
security_requirements = operation.get("security", [])
@@ -147,7 +150,8 @@ def _extract_auth(self, operation: Dict[str, Any]) -> Optional[Auth]:
return None
def _get_security_schemes(self) -> Dict[str, Any]:
- """Gets security schemes supporting both OpenAPI 2.0 and 3.0."""
+ """
+ Gets security schemes supporting both OpenAPI 2.0 and 3.0."""
# OpenAPI 3.0 format
if "components" in self.spec:
return self.spec.get("components", {}).get("securitySchemes", {})
diff --git a/plugins/communication_protocols/http/src/utcp_http/sse_call_template.py b/plugins/communication_protocols/http/src/utcp_http/sse_call_template.py
index 9414921..6c04e23 100644
--- a/plugins/communication_protocols/http/src/utcp_http/sse_call_template.py
+++ b/plugins/communication_protocols/http/src/utcp_http/sse_call_template.py
@@ -7,7 +7,8 @@
from pydantic import Field
class SseCallTemplate(CallTemplate):
- """Provider configuration for Server-Sent Events (SSE) tools.
+ """REQUIRED
+ Provider configuration for Server-Sent Events (SSE) tools.
Enables real-time streaming of events from server to client using the
Server-Sent Events protocol. Supports automatic reconnection and
@@ -38,12 +39,17 @@ class SseCallTemplate(CallTemplate):
class SSECallTemplateSerializer(Serializer[SseCallTemplate]):
- """Serializer for SSECallTemplate."""
+ """REQUIRED
+ Serializer for SSECallTemplate."""
def to_dict(self, obj: SseCallTemplate) -> dict:
+ """REQUIRED
+ Converts a SSECallTemplate to a dictionary."""
return obj.model_dump()
def validate_dict(self, obj: dict) -> SseCallTemplate:
+ """REQUIRED
+ Validates a dictionary and returns a SSECallTemplate."""
try:
return SseCallTemplate.model_validate(obj)
except Exception as e:
diff --git a/plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py b/plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py
index 6768875..d694133 100644
--- a/plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py
+++ b/plugins/communication_protocols/http/src/utcp_http/sse_communication_protocol.py
@@ -22,7 +22,8 @@
logger = logging.getLogger(__name__)
class SseCommunicationProtocol(CommunicationProtocol):
- """SSE communication protocol implementation for UTCP client.
+ """REQUIRED
+ SSE communication protocol implementation for UTCP client.
Handles Server-Sent Events based tool providers with streaming capabilities.
"""
@@ -64,7 +65,8 @@ def _apply_auth(self, provider: SseCallTemplate, headers: Dict[str, str], query_
return auth, cookies
async def register_manual(self, caller, manual_call_template: CallTemplate) -> RegisterManualResult:
- """Register a manual and its tools from an SSE provider."""
+ """REQUIRED
+ Register a manual and its tools from an SSE provider."""
if not isinstance(manual_call_template, SseCallTemplate):
raise ValueError("SSECommunicationProtocol can only be used with SSECallTemplate")
@@ -145,7 +147,8 @@ async def register_manual(self, caller, manual_call_template: CallTemplate) -> R
)
async def deregister_manual(self, caller, manual_call_template: CallTemplate) -> None:
- """Deregister an SSE manual and close any active connections."""
+ """REQUIRED
+ Deregister an SSE manual and close any active connections."""
template_name = manual_call_template.name
if template_name in self._active_connections:
response, session = self._active_connections.pop(template_name)
@@ -153,7 +156,8 @@ async def deregister_manual(self, caller, manual_call_template: CallTemplate) ->
await session.close()
async def call_tool(self, caller, tool_name: str, tool_args: Dict[str, Any], tool_call_template: CallTemplate) -> Any:
- """Execute a tool call through SSE transport."""
+ """REQUIRED
+ Execute a tool call through SSE transport."""
if not isinstance(tool_call_template, SseCallTemplate):
raise ValueError("SSECommunicationProtocol can only be used with SSECallTemplate")
@@ -163,7 +167,8 @@ async def call_tool(self, caller, tool_name: str, tool_args: Dict[str, Any], too
return event_list
async def call_tool_streaming(self, caller, tool_name: str, tool_args: Dict[str, Any], tool_call_template: CallTemplate) -> AsyncGenerator[Any, None]:
- """Execute a tool call through SSE transport with streaming."""
+ """REQUIRED
+ Execute a tool call through SSE transport with streaming."""
if not isinstance(tool_call_template, SseCallTemplate):
raise ValueError("SSECommunicationProtocol can only be used with SSECallTemplate")
diff --git a/plugins/communication_protocols/http/src/utcp_http/streamable_http_call_template.py b/plugins/communication_protocols/http/src/utcp_http/streamable_http_call_template.py
index caf62ae..be2ba1a 100644
--- a/plugins/communication_protocols/http/src/utcp_http/streamable_http_call_template.py
+++ b/plugins/communication_protocols/http/src/utcp_http/streamable_http_call_template.py
@@ -7,7 +7,8 @@
from pydantic import Field
class StreamableHttpCallTemplate(CallTemplate):
- """Provider configuration for HTTP streaming tools.
+ """REQUIRED
+ Provider configuration for HTTP streaming tools.
Uses HTTP Chunked Transfer Encoding to enable streaming of large responses
or real-time data. Useful for tools that return large datasets or provide
@@ -40,12 +41,17 @@ class StreamableHttpCallTemplate(CallTemplate):
class StreamableHttpCallTemplateSerializer(Serializer[StreamableHttpCallTemplate]):
- """Serializer for StreamableHttpCallTemplate."""
+ """REQUIRED
+ Serializer for StreamableHttpCallTemplate."""
def to_dict(self, obj: StreamableHttpCallTemplate) -> dict:
+ """REQUIRED
+ Converts a StreamableHttpCallTemplate to a dictionary."""
return obj.model_dump()
def validate_dict(self, obj: dict) -> StreamableHttpCallTemplate:
+ """REQUIRED
+ Validates a dictionary and returns a StreamableHttpCallTemplate."""
try:
return StreamableHttpCallTemplate.model_validate(obj)
except Exception as e:
diff --git a/plugins/communication_protocols/http/src/utcp_http/streamable_http_communication_protocol.py b/plugins/communication_protocols/http/src/utcp_http/streamable_http_communication_protocol.py
index 47144f8..947470f 100644
--- a/plugins/communication_protocols/http/src/utcp_http/streamable_http_communication_protocol.py
+++ b/plugins/communication_protocols/http/src/utcp_http/streamable_http_communication_protocol.py
@@ -19,7 +19,8 @@
logger = logging.getLogger(__name__)
class StreamableHttpCommunicationProtocol(CommunicationProtocol):
- """Streamable HTTP communication protocol implementation for UTCP client.
+ """REQUIRED
+ Streamable HTTP communication protocol implementation for UTCP client.
Handles HTTP streaming with chunked transfer encoding for real-time data.
"""
@@ -65,7 +66,8 @@ async def close(self):
self._oauth_tokens.clear()
async def register_manual(self, caller, manual_call_template: CallTemplate) -> RegisterManualResult:
- """Register a manual and its tools from a StreamableHttp provider."""
+ """REQUIRED
+ Register a manual and its tools from a StreamableHttp provider."""
if not isinstance(manual_call_template, StreamableHttpCallTemplate):
raise ValueError("StreamableHttpCommunicationProtocol can only be used with StreamableHttpCallTemplate")
@@ -165,11 +167,13 @@ async def register_manual(self, caller, manual_call_template: CallTemplate) -> R
)
async def deregister_manual(self, caller, manual_call_template: CallTemplate) -> None:
- """Deregister a StreamableHttp manual. This is a no-op for the stateless streamable HTTP protocol."""
+ """REQUIRED
+ Deregister a StreamableHttp manual. This is a no-op for the stateless streamable HTTP protocol."""
logger.info(f"Deregistering manual '{manual_call_template.name}'. No active connection to close.")
async def call_tool(self, caller, tool_name: str, tool_args: Dict[str, Any], tool_call_template: CallTemplate) -> Any:
- """Execute a tool call through StreamableHttp transport."""
+ """REQUIRED
+ Execute a tool call through StreamableHttp transport."""
if not isinstance(tool_call_template, StreamableHttpCallTemplate):
raise ValueError("StreamableHttpCommunicationProtocol can only be used with StreamableHttpCallTemplate")
@@ -187,7 +191,8 @@ async def call_tool(self, caller, tool_name: str, tool_args: Dict[str, Any], too
return chunk_list
async def call_tool_streaming(self, caller, tool_name: str, tool_args: Dict[str, Any], tool_call_template: CallTemplate) -> AsyncGenerator[Any, None]:
- """Execute a tool call through StreamableHttp transport with streaming."""
+ """REQUIRED
+ Execute a tool call through StreamableHttp transport with streaming."""
if not isinstance(tool_call_template, StreamableHttpCallTemplate):
raise ValueError("StreamableHttpCommunicationProtocol can only be used with StreamableHttpCallTemplate")
diff --git a/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_call_template.py b/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_call_template.py
index c62901f..5755804 100644
--- a/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_call_template.py
+++ b/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_call_template.py
@@ -14,7 +14,11 @@
"""
class McpConfig(BaseModel):
- """Configuration container for multiple MCP servers.
+ """REQUIRED
+ Implementing this class is not required!!!
+ The McpCallTemplate just needs to support a MCP compliant server configuration.
+
+ Configuration container for multiple MCP servers.
Holds a collection of named MCP server configurations, allowing
a single MCP provider to manage multiple server connections.
@@ -26,7 +30,8 @@ class McpConfig(BaseModel):
mcpServers: Dict[str, Dict[str, Any]]
class McpCallTemplate(CallTemplate):
- """Provider configuration for Model Context Protocol (MCP) tools.
+ """REQUIRED
+ Provider configuration for Model Context Protocol (MCP) tools.
Enables communication with MCP servers that provide structured tool
interfaces. Supports both stdio (local process) and HTTP (remote)
@@ -44,10 +49,19 @@ class McpCallTemplate(CallTemplate):
auth: Optional[OAuth2Auth] = None
class McpCallTemplateSerializer(Serializer[McpCallTemplate]):
+ """REQUIRED
+ Serializer for McpCallTemplate.
+ """
def to_dict(self, obj: McpCallTemplate) -> dict:
+ """REQUIRED
+ Convert McpCallTemplate to dictionary.
+ """
return obj.model_dump()
def validate_dict(self, obj: dict) -> McpCallTemplate:
+ """REQUIRED
+ Validate and convert dictionary to McpCallTemplate.
+ """
try:
return McpCallTemplate.model_validate(obj)
except Exception as e:
diff --git a/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py b/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py
index 6daf489..261a693 100644
--- a/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py
+++ b/plugins/communication_protocols/mcp/src/utcp_mcp/mcp_communication_protocol.py
@@ -20,7 +20,8 @@
logger = logging.getLogger(__name__)
class McpCommunicationProtocol(CommunicationProtocol):
- """MCP transport implementation that connects to MCP servers via stdio or HTTP.
+ """REQUIRED
+ MCP transport implementation that connects to MCP servers via stdio or HTTP.
This implementation uses a session-per-operation approach where each operation
(register, call_tool) opens a fresh session, performs the operation, and closes.
@@ -84,6 +85,9 @@ async def _call_tool_with_session(self, server_config: Dict[str, Any], tool_name
raise ValueError(f"Unsupported MCP transport: {json.dumps(server_config)}")
async def register_manual(self, caller: 'UtcpClient', manual_call_template: CallTemplate) -> RegisterManualResult:
+ """REQUIRED
+ Register a manual with the communication protocol.
+ """
if not isinstance(manual_call_template, McpCallTemplate):
raise ValueError("manual_call_template must be a McpCallTemplate")
all_tools = []
@@ -117,6 +121,9 @@ async def register_manual(self, caller: 'UtcpClient', manual_call_template: Call
)
async def call_tool(self, caller: 'UtcpClient', tool_name: str, tool_args: Dict[str, Any], tool_call_template: CallTemplate) -> Any:
+ """REQUIRED
+ Call a tool using the model context protocol.
+ """
if not isinstance(tool_call_template, McpCallTemplate):
raise ValueError("tool_call_template must be a McpCallTemplate")
if not tool_call_template.config or not tool_call_template.config.mcpServers:
@@ -147,6 +154,8 @@ async def call_tool(self, caller: 'UtcpClient', tool_name: str, tool_args: Dict[
raise ValueError(f"Tool '{tool_name}' not found in any configured server")
async def call_tool_streaming(self, caller: 'UtcpClient', tool_name: str, tool_args: Dict[str, Any], tool_call_template: CallTemplate) -> AsyncGenerator[Any, None]:
+ """REQUIRED
+ Streaming calls are not supported for MCP protocol, so we just call the tool and return the result as one item."""
yield self.call_tool(caller, tool_name, tool_args, tool_call_template)
def _process_tool_result(self, result, tool_name: str) -> Any:
diff --git a/plugins/communication_protocols/text/src/utcp_text/text_call_template.py b/plugins/communication_protocols/text/src/utcp_text/text_call_template.py
index ee9af09..23ba009 100644
--- a/plugins/communication_protocols/text/src/utcp_text/text_call_template.py
+++ b/plugins/communication_protocols/text/src/utcp_text/text_call_template.py
@@ -7,7 +7,8 @@
import traceback
class TextCallTemplate(CallTemplate):
- """Call template for text file-based manuals and tools.
+ """REQUIRED
+ Call template for text file-based manuals and tools.
Reads UTCP manuals or tool definitions from local JSON/YAML files. Useful for
static tool configurations or environments where manuals are distributed as files.
@@ -24,12 +25,17 @@ class TextCallTemplate(CallTemplate):
class TextCallTemplateSerializer(Serializer[TextCallTemplate]):
- """Serializer for TextCallTemplate."""
+ """REQUIRED
+ Serializer for TextCallTemplate."""
def to_dict(self, obj: TextCallTemplate) -> dict:
+ """REQUIRED
+ Convert a TextCallTemplate to a dictionary."""
return obj.model_dump()
def validate_dict(self, obj: dict) -> TextCallTemplate:
+ """REQUIRED
+ Validate and convert a dictionary to a TextCallTemplate."""
try:
return TextCallTemplate.model_validate(obj)
except Exception as e:
diff --git a/plugins/communication_protocols/text/src/utcp_text/text_communication_protocol.py b/plugins/communication_protocols/text/src/utcp_text/text_communication_protocol.py
index 2ef89e9..4a66b56 100644
--- a/plugins/communication_protocols/text/src/utcp_text/text_communication_protocol.py
+++ b/plugins/communication_protocols/text/src/utcp_text/text_communication_protocol.py
@@ -26,7 +26,8 @@
logger = logging.getLogger(__name__)
class TextCommunicationProtocol(CommunicationProtocol):
- """Communication protocol for file-based UTCP manuals and tools."""
+ """REQUIRED
+ Communication protocol for file-based UTCP manuals and tools."""
def _log_info(self, message: str) -> None:
logger.info(f"[TextCommunicationProtocol] {message}")
@@ -35,7 +36,8 @@ def _log_error(self, message: str) -> None:
logger.error(f"[TextCommunicationProtocol Error] {message}")
async def register_manual(self, caller: 'UtcpClient', manual_call_template: CallTemplate) -> RegisterManualResult:
- """Register a text manual and return its tools as a UtcpManual."""
+ """REQUIRED
+ Register a text manual and return its tools as a UtcpManual."""
if not isinstance(manual_call_template, TextCallTemplate):
raise ValueError("TextCommunicationProtocol requires a TextCallTemplate")
@@ -94,12 +96,14 @@ async def register_manual(self, caller: 'UtcpClient', manual_call_template: Call
)
async def deregister_manual(self, caller: 'UtcpClient', manual_call_template: CallTemplate) -> None:
- """Deregister a text manual (no-op)."""
+ """REQUIRED
+ Deregister a text manual (no-op)."""
if isinstance(manual_call_template, TextCallTemplate):
self._log_info(f"Deregistering text manual '{manual_call_template.name}' (no-op)")
async def call_tool(self, caller: 'UtcpClient', tool_name: str, tool_args: Dict[str, Any], tool_call_template: CallTemplate) -> Any:
- """Call a tool: for text templates, return file content from the configured path."""
+ """REQUIRED
+ Call a tool: for text templates, return file content from the configured path."""
if not isinstance(tool_call_template, TextCallTemplate):
raise ValueError("TextCommunicationProtocol requires a TextCallTemplate for tool calls")
@@ -118,6 +122,7 @@ async def call_tool(self, caller: 'UtcpClient', tool_name: str, tool_args: Dict[
raise
async def call_tool_streaming(self, caller: 'UtcpClient', tool_name: str, tool_args: Dict[str, Any], tool_call_template: CallTemplate) -> AsyncGenerator[Any, None]:
- """Streaming variant: yields the full content as a single chunk."""
+ """REQUIRED
+ Streaming variant: yields the full content as a single chunk."""
result = await self.call_tool(caller, tool_name, tool_args, tool_call_template)
yield result
diff --git a/scripts/extract_required_docs.py b/scripts/extract_required_docs.py
new file mode 100644
index 0000000..af6bfbb
--- /dev/null
+++ b/scripts/extract_required_docs.py
@@ -0,0 +1,966 @@
+#!/usr/bin/env python3
+"""
+Script to extract REQUIRED docstrings from UTCP codebase and generate Docusaurus documentation.
+
+This script scans all Python files in core/ and plugins/ directories, extracts docstrings
+that start with "REQUIRED", and generates organized Docusaurus markdown files.
+"""
+
+import ast
+import os
+import re
+from pathlib import Path
+from typing import Dict, List, Tuple, Optional
+from dataclasses import dataclass
+
+
+@dataclass
+class DocEntry:
+ """Represents a documentation entry extracted from code."""
+ name: str
+ type: str # 'module', 'class', 'function', 'method'
+ docstring: str
+ file_path: str
+ line_number: int
+ parent_class: Optional[str] = None
+ signature: Optional[str] = None # Function/method signature
+ class_fields: Optional[List[str]] = None # Non-private class attributes
+ base_classes: Optional[List[str]] = None # Parent classes (excluding Python built-ins)
+
+
+class RequiredDocExtractor:
+ """Extracts REQUIRED docstrings from Python files and generates Docusaurus docs."""
+
+ def __init__(self, root_path: str):
+ self.root_path = Path(root_path)
+ self.doc_entries: List[DocEntry] = []
+ self.class_index: Dict[str, str] = {} # class_name -> file_path mapping
+ self.output_file_mapping: Dict[str, str] = {} # source_file_path -> output_file_path mapping
+
+ def is_required_docstring(self, docstring: str) -> bool:
+ """Check if docstring starts with REQUIRED."""
+ if not docstring:
+ return False
+ return docstring.strip().startswith("REQUIRED")
+
+ def clean_docstring(self, docstring: str) -> str:
+ """Clean and format docstring for markdown output."""
+ if not docstring:
+ return ""
+
+ # Remove REQUIRED prefix
+ lines = docstring.strip().split('\n')
+ if lines[0].strip() == "REQUIRED":
+ lines = lines[1:]
+ elif lines[0].strip().startswith("REQUIRED"):
+ lines[0] = lines[0].replace("REQUIRED", "", 1).strip()
+
+ # Remove common indentation
+ if lines:
+ # Find minimum indentation (excluding empty lines)
+ non_empty_lines = [line for line in lines if line.strip()]
+ if non_empty_lines:
+ min_indent = min(len(line) - len(line.lstrip()) for line in non_empty_lines)
+ lines = [line[min_indent:] if line.strip() else line for line in lines]
+
+ return '\n'.join(lines).strip()
+
+ def convert_docstring_to_html_markdown(self, docstring: str) -> str:
+ """Convert Google-style docstring to HTML markdown for Docusaurus.
+
+ Args:
+ docstring: The raw docstring text
+
+ Returns:
+ HTML markdown formatted string suitable for Docusaurus
+ """
+ if not docstring:
+ return "*No documentation available*"
+
+ if docstring.startswith("REQUIRED"):
+ docstring = docstring.replace("REQUIRED", "", 1).strip()
+ if docstring.startswith("\n"):
+ docstring = docstring[1:]
+
+ lines = docstring.split('\n')
+ result = []
+ current_section = None
+ current_section_content = []
+
+ # Common Google-style section headers
+ section_headers = {
+ 'args:', 'arguments:', 'parameters:', 'param:', 'params:',
+ 'returns:', 'return:', 'yields:', 'yield:',
+ 'raises:', 'except:', 'exceptions:',
+ 'examples:', 'example:',
+ 'note:', 'notes:',
+ 'warning:', 'warnings:',
+ 'see also:', 'seealso:',
+ 'attributes:', 'attr:', 'attrs:',
+ 'methods:', 'method:',
+ 'properties:', 'property:', 'props:'
+ }
+
+ def process_section_content(content_lines):
+ """Process content lines within a section."""
+ if not content_lines:
+ return []
+
+ processed = []
+ i = 0
+ in_code_block = False
+
+ while i < len(content_lines):
+ line = content_lines[i]
+ stripped = line.strip()
+
+ # Check for code block delimiters
+ if stripped.startswith('```'):
+ # Check if code block is started and closed on the same line
+ if stripped.count('```') >= 2:
+ processed.append(stripped)
+ i += 1
+ continue
+ else:
+ in_code_block = not in_code_block
+ processed.append(stripped)
+ i += 1
+ continue
+
+ # If we're inside a code block, preserve the line as-is
+ if in_code_block:
+ processed.append(line.rstrip())
+ i += 1
+ continue
+
+ # Skip empty lines
+ if not stripped:
+ processed.append('')
+ i += 1
+ continue
+
+ # Clean up multiple consecutive empty lines
+ while '\n\n\n' in line:
+ line = line.replace('\n\n\n', '\n\n')
+ stripped = line.strip()
+
+ # Escape any remaining curly braces for Docusaurus
+ line = line.replace('{', '\\{').replace('}', '\\}')
+ stripped = line.strip()
+
+ # Check if this looks like a parameter/item definition (name: description)
+ if ':' in stripped and not stripped.endswith(':'):
+ colon_pos = stripped.find(':')
+ param_name = stripped[:colon_pos].strip()
+ param_desc = stripped[colon_pos + 1:].strip()
+
+ # Check if param_name looks like a parameter (no spaces, reasonable length)
+ if ' ' not in param_name and len(param_name) <= 50 and param_name.replace('_', '').isalnum():
+ # This is likely a parameter definition
+ processed.append(f"- **`{param_name}`**: {param_desc}")
+
+ # Check for continuation lines (indented more than the parameter line)
+ base_indent = len(line) - len(line.lstrip())
+ i += 1
+ while i < len(content_lines):
+ next_line = content_lines[i]
+ next_stripped = next_line.strip()
+ next_indent = len(next_line) - len(next_line.lstrip()) if next_stripped else 0
+
+ # Check if we hit a code block
+ if next_stripped.startswith('```'):
+ break
+
+ if not next_stripped:
+ # Empty line - add it and continue
+ processed.append('')
+ i += 1
+ elif next_indent > base_indent:
+ # Continuation line - add with proper spacing
+ processed.append(f" {next_stripped}")
+ i += 1
+ else:
+ # Not a continuation, back up and break
+ break
+ continue
+
+ # Check if line starts with a list marker
+ elif stripped.startswith(('- ', '* ', '+ ')):
+ # This is already a markdown list item
+ processed.append(stripped)
+ elif stripped.startswith(('1. ', '2. ', '3. ', '4. ', '5. ', '6. ', '7. ', '8. ', '9. ')):
+ # Numbered list item
+ processed.append(stripped)
+ else:
+ # Regular paragraph text
+ processed.append(stripped)
+
+ i += 1
+
+ return processed
+
+ if docstring.__contains__('{VAR}'):
+ print("")
+ # Parse the docstring line by line
+ for line in lines:
+ stripped_lower = line.strip().lower()
+
+ # Check if this line is a section header
+ if stripped_lower in section_headers or stripped_lower.endswith(':'):
+ # Save previous section if it exists
+ if current_section:
+ processed_content = process_section_content(current_section_content)
+ if processed_content:
+ result.append(f"\n**{current_section.title()}**\n")
+ result.extend(processed_content)
+ result.append('')
+ else:
+ processed_content = process_section_content(current_section_content)
+ if processed_content:
+ result.extend(processed_content)
+
+ # Start new section
+ current_section = line.strip().rstrip(':')
+ current_section_content = []
+ else:
+ current_section_content.append(line)
+
+ # Process the last section
+ if current_section:
+ processed_content = process_section_content(current_section_content)
+ if processed_content:
+ result.append(f"\n**{current_section.title()}**\n")
+ result.extend(processed_content)
+
+ # Clean up the result
+ final_result = []
+ for line in result:
+ if isinstance(line, str):
+ final_result.append(line)
+
+ # Join and clean up extra whitespace
+ markdown_text = '\n'.join(final_result)
+
+ return markdown_text.strip()
+
+ def get_function_signature(self, node: ast.FunctionDef) -> str:
+ """Extract function signature from AST node."""
+ try:
+ # Handle both sync and async functions
+ prefix = "async " if isinstance(node, ast.AsyncFunctionDef) else ""
+
+ # Get function name
+ sig_parts = [prefix + node.name + "("]
+
+ # Process arguments
+ args = []
+
+ # Regular arguments
+ for arg in node.args.args:
+ arg_str = arg.arg
+ if arg.annotation:
+ arg_str += f": {ast.unparse(arg.annotation)}"
+ args.append(arg_str)
+
+ # *args
+ if node.args.vararg:
+ vararg_str = f"*{node.args.vararg.arg}"
+ if node.args.vararg.annotation:
+ vararg_str += f": {ast.unparse(node.args.vararg.annotation)}"
+ args.append(vararg_str)
+
+ # **kwargs
+ if node.args.kwarg:
+ kwarg_str = f"**{node.args.kwarg.arg}"
+ if node.args.kwarg.annotation:
+ kwarg_str += f": {ast.unparse(node.args.kwarg.annotation)}"
+ args.append(kwarg_str)
+
+ sig_parts.append(", ".join(args))
+ sig_parts.append(")")
+
+ # Return type annotation
+ if node.returns:
+ sig_parts.append(f" -> {ast.unparse(node.returns)}")
+
+ return "".join(sig_parts)
+ except Exception:
+ # Fallback to simple signature
+ prefix = "async " if isinstance(node, ast.AsyncFunctionDef) else ""
+ return f"{prefix}{node.name}(...)"
+
+ def get_class_fields(self, node: ast.ClassDef) -> List[str]:
+ """Extract non-private class fields from AST node."""
+ fields = []
+
+ for item in node.body:
+ if isinstance(item, ast.AnnAssign) and isinstance(item.target, ast.Name):
+ # Type annotated attribute
+ field_name = item.target.id
+ if not field_name.startswith('_'): # Skip private fields
+ annotation = ast.unparse(item.annotation) if item.annotation else ""
+ fields.append(f"{field_name}: {annotation}")
+ elif isinstance(item, ast.Assign):
+ # Regular assignment
+ for target in item.targets:
+ if isinstance(target, ast.Name) and not target.id.startswith('_'):
+ fields.append(target.id)
+
+ return fields
+
+ def get_class_base_classes(self, node: ast.ClassDef) -> List[str]:
+ """Extract base classes from AST node, excluding Python built-ins."""
+ # Common Python built-ins to exclude
+ exclude_bases = {
+ 'ABC', 'BaseModel', 'object', 'Exception', 'BaseException',
+ 'dict', 'list', 'str', 'int', 'float', 'bool', 'tuple', 'set',
+ 'Generic', 'Enum', 'IntEnum', 'NamedTuple'
+ }
+
+ base_classes = []
+ for base in node.bases:
+ try:
+ base_name = ast.unparse(base)
+ # Extract just the class name if it's a complex expression
+ if '.' in base_name:
+ base_name = base_name.split('.')[-1]
+
+ if base_name not in exclude_bases:
+ base_classes.append(base_name)
+ except Exception:
+ # Skip if we can't parse the base class
+ pass
+
+ return base_classes
+
+ def extract_from_file(self, file_path: Path) -> List[DocEntry]:
+ """Extract REQUIRED docstrings from a single Python file."""
+ entries = []
+
+ try:
+ with open(file_path, 'r', encoding='utf-8') as f:
+ content = f.read()
+
+ # Parse AST
+ tree = ast.parse(content, filename=str(file_path))
+
+ # Extract module-level docstring
+ module_docstring = ast.get_docstring(tree)
+ if self.is_required_docstring(module_docstring):
+ entries.append(DocEntry(
+ name=file_path.stem,
+ type='module',
+ docstring=self.convert_docstring_to_html_markdown(module_docstring),
+ file_path=str(file_path.relative_to(self.root_path)).replace('\\', '/'),
+ line_number=1
+ ))
+
+ # Track class methods to avoid duplicating them as functions
+ class_methods = set()
+
+ # First pass: extract classes and methods
+ for node in ast.walk(tree):
+ if isinstance(node, ast.ClassDef):
+ class_docstring = ast.get_docstring(node)
+ if self.is_required_docstring(class_docstring):
+ class_fields = self.get_class_fields(node)
+ base_classes = self.get_class_base_classes(node)
+ entries.append(DocEntry(
+ name=node.name,
+ type='class',
+ docstring=self.convert_docstring_to_html_markdown(class_docstring),
+ file_path=str(file_path.relative_to(self.root_path)).replace('\\', '/'),
+ line_number=node.lineno,
+ class_fields=class_fields,
+ base_classes=base_classes
+ ))
+
+ # Extract methods from class (both sync and async)
+ for item in node.body:
+ if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
+ class_methods.add(id(item)) # Track this method
+ method_docstring = ast.get_docstring(item)
+ if self.is_required_docstring(method_docstring):
+ signature = self.get_function_signature(item)
+ if signature.__contains__('find_required_variables'):
+ print("test")
+ entries.append(DocEntry(
+ name=item.name,
+ type='method',
+ docstring=self.convert_docstring_to_html_markdown(method_docstring),
+ file_path=str(file_path.relative_to(self.root_path)).replace('\\', '/'),
+ line_number=item.lineno,
+ parent_class=node.name,
+ signature=signature
+ ))
+
+ # Second pass: extract top-level functions (not already processed as methods)
+ for node in ast.walk(tree):
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)) and id(node) not in class_methods:
+ func_docstring = ast.get_docstring(node)
+ if self.is_required_docstring(func_docstring):
+ signature = self.get_function_signature(node)
+ entries.append(DocEntry(
+ name=node.name,
+ type='function',
+ docstring=self.convert_docstring_to_html_markdown(func_docstring),
+ file_path=str(file_path.relative_to(self.root_path)).replace('\\', '/'),
+ line_number=node.lineno,
+ signature=signature
+ ))
+
+ except Exception as e:
+ print(f"Error processing {file_path}: {e}")
+
+ return entries
+
+ def scan_directories(self, directories: List[str]) -> None:
+ """Scan specified directories for Python files."""
+ for directory in directories:
+ dir_path = self.root_path / directory
+ if not dir_path.exists():
+ print(f"Warning: Directory {dir_path} does not exist")
+ continue
+
+ for py_file in dir_path.rglob("*.py"):
+ entries = self.extract_from_file(py_file)
+ self.doc_entries.extend(entries)
+
+ # Build class index for cross-references
+ for entry in entries:
+ if entry.type == 'class':
+ self.class_index[entry.name] = entry.file_path.replace('\\', '/')
+
+ def organize_by_module(self) -> Dict[str, Dict[str, List[DocEntry]]]:
+ """Organize documentation entries by module/file."""
+ modules = {}
+
+ for entry in self.doc_entries:
+ file_key = entry.file_path.replace('\\', '/')
+ if file_key not in modules:
+ modules[file_key] = {
+ 'module': [],
+ 'classes': [],
+ 'functions': [],
+ 'methods': []
+ }
+
+ if entry.type == 'module':
+ modules[file_key]['module'].append(entry)
+ elif entry.type == 'class':
+ modules[file_key]['classes'].append(entry)
+ elif entry.type == 'function':
+ modules[file_key]['functions'].append(entry)
+ elif entry.type == 'method':
+ modules[file_key]['methods'].append(entry)
+
+ # Sort entries within each file
+ for file_data in modules.values():
+ for category in file_data.values():
+ category.sort(key=lambda x: x.line_number)
+
+ return modules
+
+ def add_cross_references(self, text: str, current_file_path: str) -> str:
+ """Placeholder method for cross-references during first pass generation."""
+ # During first pass, we don't have output file paths yet
+ # All cross-referencing will be done in post-generation step
+ return text
+
+ def format_field_with_references(self, field: str, current_file_path: str) -> str:
+ """Format a field with proper cross-references and styling."""
+ if ':' not in field:
+ return f"`{field}`"
+
+ field_name, field_type = field.split(':', 1)
+ field_name = field_name.strip()
+ field_type = field_type.strip()
+
+ # Will be replaced with actual links after file generation
+ return f"`{field_name}: {field_type}`"
+
+ def add_cross_references_post_generation(self, text: str, current_output_file: str) -> str:
+ """Add cross-references using actual output file paths."""
+ if not text:
+ return text
+
+ modified_text = text
+ for class_name, source_file_path in self.class_index.items():
+ pattern = r'\b' + re.escape(class_name) + r'\b'
+ if re.search(pattern, modified_text):
+ target_output_file = self.output_file_mapping.get(source_file_path)
+ if not target_output_file:
+ continue
+
+ if target_output_file == current_output_file:
+ pass
+ # Same file - just anchor
+ # class_anchor = re.sub(r'[^\w\-_]', '-', class_name.lower()).strip('-')
+ # link = f"[{class_name}](#{class_anchor})"
+ link = class_name
+ else:
+ # Different file - calculate actual relative path
+ current_dir = Path(current_output_file).parent
+ target_path = Path(target_output_file)
+
+ try:
+ relative_path = str(target_path.relative_to(current_dir)).replace('\\', '/')
+ class_anchor = re.sub(r'[^\w\-_]', '-', class_name.lower()).strip('-')
+ link = f"[{class_name}](./{relative_path}#{class_anchor})"
+ except ValueError:
+ # Files are in different trees, calculate with .. navigation
+ current_parts = current_dir.parts
+ target_parts = target_path.parent.parts
+
+ # Find common prefix
+ common_len = 0
+ for i in range(min(len(current_parts), len(target_parts))):
+ if current_parts[i] == target_parts[i]:
+ common_len += 1
+ else:
+ break
+
+ # Build relative path
+ up_steps = len(current_parts) - common_len
+ down_steps = target_parts[common_len:]
+
+ path_components = ['..'] * up_steps + list(down_steps) + [target_path.name]
+ relative_path_str = '/'.join(path_components)
+
+ class_anchor = re.sub(r'[^\w\-_]', '-', class_name.lower()).strip('-')
+ link = f"[{class_name}](./{relative_path_str}#{class_anchor})"
+
+ # Don't replace matches that are in code blocks
+ lines = modified_text.split('\n')
+ in_code_block = False
+ for i, line in enumerate(lines):
+ if line.strip().startswith('```'):
+ in_code_block = not in_code_block
+ elif not in_code_block:
+ lines[i] = re.sub(pattern, link, line)
+ modified_text = '\n'.join(lines)
+
+ return modified_text
+
+ def generate_module_markdown(self, file_path: str, file_data: Dict[str, List[DocEntry]]) -> str:
+ """Generate markdown content for a single module/file."""
+ if not any(file_data.values()):
+ return ""
+
+ # Clean up file path for display
+ display_path = file_path
+ if display_path.startswith('core/src/'):
+ display_path = display_path[9:] # Remove 'core/src/' prefix
+ elif display_path.startswith('plugins/'):
+ display_path = display_path[8:] # Remove 'plugins/' prefix
+
+ # Create title from file name only
+ title = Path(display_path).stem
+
+ content = [
+ "---",
+ f"title: {title}",
+ f"sidebar_label: {title}",
+ "---",
+ "",
+ f"# {title}",
+ "",
+ f"**File:** `{file_path}`",
+ "",
+ ]
+
+ # Add module docstring if present
+ if file_data['module']:
+ module_entry = file_data['module'][0]
+ content.extend([
+ "## Module Description",
+ "",
+ module_entry.docstring if module_entry.docstring else "*No module documentation available*",
+ "",
+ ])
+
+ # Group methods by their parent class
+ methods_by_class = {}
+ for method in file_data['methods']:
+ class_name = method.parent_class or 'Unknown'
+ if class_name not in methods_by_class:
+ methods_by_class[class_name] = []
+ methods_by_class[class_name].append(method)
+
+ # Add classes with their methods
+ if file_data['classes']:
+ for class_entry in file_data['classes']:
+ # Create anchor-friendly ID
+ class_anchor = re.sub(r'[^\w\-_]', '-', class_entry.name.lower()).strip('-')
+
+ # Create class header with optional parent classes in parentheses
+ class_header = f"### class {class_entry.name}"
+ if class_entry.base_classes:
+ base_classes_with_links = []
+ for base_class in class_entry.base_classes:
+ linked_base = self.add_cross_references(base_class, file_path)
+ base_classes_with_links.append(linked_base)
+ class_header += f" ({', '.join(base_classes_with_links)})"
+ class_header += f" {{#{class_anchor}}}"
+
+ content.extend([
+ class_header,
+ "",
+ ])
+
+ # Add class docstring
+ if class_entry.docstring:
+ content.extend([
+ "",
+ "Documentation
",
+ "",
+ class_entry.docstring,
+
+ " ",
+ "",
+ ])
+ else:
+ content.extend(["*No class documentation available*", ""])
+
+ # Add class fields if available
+ if class_entry.class_fields:
+ content.extend(["#### Fields:", ""])
+ for field in class_entry.class_fields:
+ formatted_field = self.format_field_with_references(field, file_path)
+ content.append(f"- {formatted_field}")
+ content.append("")
+
+ # Add methods for this class
+ if class_entry.name in methods_by_class:
+ content.extend(["#### Methods:", ""])
+
+ for method in methods_by_class[class_entry.name]:
+ method_anchor = re.sub(r'[^\w\-_]', '-', f"{class_entry.name}-{method.name}".lower()).strip('-')
+
+ # Add cross-references to method signature
+ linked_signature = self.add_cross_references(method.signature, file_path)
+
+ docstrings = ""
+
+ if method.docstring:
+ docstrings = method.docstring
+ else:
+ docstrings = "*No method documentation available*"
+
+ content.extend(
+ [
+ "",
+ f"{linked_signature}
",
+ "",
+ docstrings,
+ " ",
+ "",
+ ]
+ )
+
+ content.extend(["---", ""])
+
+ # Add standalone functions
+ if file_data['functions']:
+ for func_entry in file_data['functions']:
+ func_anchor = re.sub(r'[^\w\-_]', '-', func_entry.name.lower()).strip('-')
+
+ # Add cross-references to function signature
+ linked_signature = self.add_cross_references(func_entry.signature, file_path)
+
+ content.extend([
+ f"### Function {linked_signature} {{#{func_anchor}}}",
+ "",
+ ])
+
+ if func_entry.docstring:
+ content.extend([
+ "",
+ "Documentation
",
+ "",
+ func_entry.docstring,
+ " ",
+ "",
+ ])
+ else:
+ content.extend(["*No function documentation available*", ""])
+
+ content.extend(["---", ""])
+
+ return '\n'.join(content)
+
+ def generate_index_file(self, modules: Dict[str, Dict[str, List[DocEntry]]], output_path: Path) -> str:
+ """Generate the main index file."""
+ total_entries = sum(sum(len(entries) for entries in file_data.values()) for file_data in modules.values())
+
+ content = [
+ "---",
+ "title: UTCP API Reference",
+ "sidebar_label: API Specification",
+ "---",
+ "",
+ "# UTCP API Reference",
+ "",
+ "API specification of a UTCP-compliant client implementation. Any implementation of a UTCP Client needs to have all of the classes, functions and fields described in this specification.",
+ "",
+ "This specification is organized by module of the reference python implementation to provide a comprehensive understanding of UTCP's architecture.",
+ "",
+ "**Note:** The modules don't have to be implemented in the same way as in the reference implementation, but all of the functionality here needs to be provided.",
+ "",
+ f"**Total documented items:** {total_entries}",
+ f"**Modules documented:** {len(modules)}",
+ ""
+ ]
+
+ # Group modules by category
+ core_modules = []
+ plugin_modules = []
+
+ for file_path in sorted(modules.keys()):
+ display_path = file_path
+ if display_path.startswith('core/src/'):
+ display_path = display_path[9:]
+ core_modules.append((file_path, display_path))
+ elif display_path.startswith('plugins/'):
+ display_path = display_path[8:]
+ plugin_modules.append((file_path, display_path))
+ else:
+ core_modules.append((file_path, display_path))
+
+ # Add core modules
+ if core_modules:
+ content.extend([
+ "## Core Modules",
+ "",
+ "Core UTCP framework components that define the fundamental interfaces and implementations.",
+ ""
+ ])
+
+ for file_path, display_path in core_modules:
+ file_data = modules[file_path]
+ total_items = sum(len(entries) for entries in file_data.values())
+ title = display_path.replace('/', '.').replace('.py', '')
+
+ # Get actual output file path and create relative link from index
+ output_file_path = self.output_file_mapping.get(file_path)
+ if output_file_path:
+ # Calculate relative path from index to the actual output file
+ index_path = output_path / "index.md"
+ target_path = Path(output_file_path)
+ try:
+ relative_path = target_path.relative_to(output_path)
+ link_path = f"./{relative_path}"
+ except ValueError:
+ # Fallback to simple filename if relative path calculation fails
+ link_path = f"./{target_path.name}"
+ else:
+ # Fallback to old method if output file path not found
+ file_anchor = title.replace('.', '-').lower()
+ link_path = f"./{file_anchor}"
+
+ content.extend([
+ f"### [{title}]({link_path})",
+ ""
+ ])
+
+ # Add summary of what's in this module
+ items = []
+ if file_data['classes']:
+ items.append(f"{len(file_data['classes'])} classes")
+ if file_data['functions']:
+ items.append(f"{len(file_data['functions'])} functions")
+ if file_data['methods']:
+ items.append(f"{len(file_data['methods'])} methods")
+
+ if items:
+ content.append(f"- **Contains:** {', '.join(items)}")
+
+ # Add module description if available
+ if file_data['module']:
+ module_desc = file_data['module'][0].docstring
+ if module_desc:
+ # Get first line of description
+ first_line = module_desc.split('\n')[0].strip()
+ content.append(f"- **Description:** {first_line}")
+
+ content.extend(["", ""])
+
+ # Add plugin modules
+ if plugin_modules:
+ content.extend([
+ "## Plugin Modules",
+ "",
+ "Plugin implementations that extend UTCP with specific transport protocols and capabilities.",
+ ""
+ ])
+
+ for file_path, display_path in plugin_modules:
+ file_data = modules[file_path]
+ title = display_path.replace('/', '.').replace('.py', '')
+
+ # Get actual output file path and create relative link from index
+ output_file_path = self.output_file_mapping.get(file_path)
+ if output_file_path:
+ # Calculate relative path from index to the actual output file
+ index_path = output_path / "index.md"
+ target_path = Path(output_file_path)
+ try:
+ relative_path = target_path.relative_to(output_path)
+ link_path = f"./{relative_path}"
+ except ValueError:
+ # Fallback to simple filename if relative path calculation fails
+ link_path = f"./{target_path.name}"
+ else:
+ # Fallback to old method if output file path not found
+ file_anchor = title.replace('.', '-').lower()
+ link_path = f"./{file_anchor}"
+
+ content.extend([
+ f"### [{title}]({link_path})",
+ ""
+ ])
+
+ # Add summary
+ items = []
+ if file_data['classes']:
+ items.append(f"{len(file_data['classes'])} classes")
+ if file_data['functions']:
+ items.append(f"{len(file_data['functions'])} functions")
+ if file_data['methods']:
+ items.append(f"{len(file_data['methods'])} methods")
+
+ if items:
+ content.append(f"- **Contains:** {', '.join(items)}")
+
+ if file_data['module']:
+ module_desc = file_data['module'][0].docstring
+ if module_desc:
+ first_line = module_desc.split('\n')[0].strip()
+ content.append(f"- **Description:** {first_line}")
+
+ content.extend(["", ""])
+
+ # Add about UTCP section
+ content.extend([
+ "## About UTCP",
+ "",
+ "The Universal Tool Calling Protocol (UTCP) is a framework for calling tools across various transport protocols.",
+ "This API reference covers all the essential interfaces, implementations, and extension points needed to:",
+ "",
+ "- **Implement** new transport protocols",
+ "- **Extend** UTCP with custom functionality",
+ "- **Integrate** UTCP into your applications",
+ "- **Understand** the complete UTCP architecture",
+ ])
+
+ return '\n'.join(content)
+
+ def generate_docs(self, output_dir: str) -> None:
+ """Generate all documentation files organized in folders."""
+ output_path = Path(output_dir)
+ output_path.mkdir(parents=True, exist_ok=True)
+
+ modules = self.organize_by_module()
+ generated_files = {} # file_path -> (content, output_file_path)
+
+ # First pass: Generate all files without cross-references and track output paths
+ for file_path, file_data in modules.items():
+ if any(file_data.values()): # Only generate if there's content
+ content = self.generate_module_markdown(file_path, file_data)
+
+ # Determine folder structure
+ display_path = file_path
+ if display_path.startswith('core/src/'):
+ display_path = display_path[9:]
+ folder_base = output_path / "core"
+ elif display_path.startswith('plugins/'):
+ display_path = display_path[8:]
+ folder_base = output_path / "plugins"
+ else:
+ folder_base = output_path / "other"
+
+ # Create folder structure based on module path
+ path_parts = display_path.replace('.py', '').split('/')
+ module_name = Path(file_path).stem # Use actual file name
+
+ # Create nested folders for the module path
+ if len(path_parts) > 1:
+ folder_path = folder_base
+ for part in path_parts[:-1]: # All parts except the last one
+ folder_path = folder_path / part
+ folder_path.mkdir(parents=True, exist_ok=True)
+ file_output_path = folder_path / f"{module_name}.md"
+ else:
+ folder_base.mkdir(parents=True, exist_ok=True)
+ file_output_path = folder_base / f"{module_name}.md"
+
+ # Store mapping for cross-references
+ self.output_file_mapping[file_path] = str(file_output_path).replace('\\', '/')
+ generated_files[file_path] = (content, file_output_path)
+
+ # Second pass: Add cross-references and write files
+ for file_path, (content, output_file_path) in generated_files.items():
+ # Post-process content to add proper cross-references
+ processed_content = self.add_cross_references_post_generation(content, str(output_file_path).replace('\\', '/'))
+ # Also process field references
+ lines = processed_content.split('\n')
+ processed_lines = []
+ for line in lines:
+ if line.strip().startswith('- `') and ':' in line:
+ # This is likely a field line - reprocess it
+ field_match = re.match(r'^(\s*)- `([^`]+)`(.*)$', line)
+ if field_match:
+ indent, field_content, rest = field_match.groups()
+ processed_lines.append(f"{indent}- {field_content}{rest}")
+ else:
+ processed_lines.append(line)
+ else:
+ processed_lines.append(line)
+
+ final_content = '\n'.join(processed_lines)
+
+ with open(output_file_path, 'w', encoding='utf-8') as f:
+ f.write(final_content)
+
+ total_items = sum(len(entries) for entries in modules[file_path].values())
+ print(f"Generated {output_file_path} with {total_items} entries")
+
+ # Generate index file
+ index_content = self.generate_index_file(modules, output_path)
+ index_path = output_path / "index.md"
+ with open(index_path, 'w', encoding='utf-8') as f:
+ f.write(index_content)
+ print(f"Generated {index_path}")
+
+ print(f"\nDocumentation generated in {output_path}")
+ print(f"Total entries: {len(self.doc_entries)}")
+ print(f"Total modules: {len(modules)}")
+
+
+def main():
+ """Main entry point."""
+ import argparse
+
+ parser = argparse.ArgumentParser(description="Extract REQUIRED docstrings and generate Docusaurus docs")
+ parser.add_argument("--root", "-r", default=".", help="Root directory of the UTCP project")
+ parser.add_argument("--output", "-o", default="./docs", help="Output directory for generated docs")
+ parser.add_argument("--dirs", "-d", nargs="+", default=["core", "plugins"],
+ help="Directories to scan (default: core plugins)")
+
+ args = parser.parse_args()
+
+ extractor = RequiredDocExtractor(args.root)
+
+ print(f"Scanning directories: {args.dirs}")
+ extractor.scan_directories(args.dirs)
+
+ if not extractor.doc_entries:
+ print("No REQUIRED docstrings found!")
+ return
+
+ print(f"Found {len(extractor.doc_entries)} REQUIRED docstrings")
+ extractor.generate_docs(args.output)
+
+
+if __name__ == "__main__":
+ main()