diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..c9daf3f --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,66 @@ +name: CI + +on: + pull_request: + branches: [ main ] + push: + branches: [ main ] + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + test: + name: Test - Python ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10", "3.11", "3.12", "3.13"] + fail-fast: false + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + cache: 'pip' + + - name: Install hatch + run: pip install hatch + + - name: Run tests + run: hatch run test + + lint: + name: Lint + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + cache: 'pip' + + - name: Install hatch + run: pip install hatch + + - name: Check formatting + run: hatch run format + + - name: Run linter + run: hatch run lint + + - name: Type check + run: hatch run typecheck diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..4f052c8 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,79 @@ +name: Publish to PyPI + +on: + release: + types: + - published + +jobs: + check: + name: Run checks + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install hatch + run: pip install hatch + + - name: Run checks + run: hatch run prepare + + build: + name: Build distribution + needs: check + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Install hatch + run: pip install hatch twine + + - name: Build package + run: hatch build + + - name: Check distribution + run: twine check dist/* + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + publish: + name: Publish to PyPI + needs: build + runs-on: ubuntu-latest + environment: + name: pypi + url: https://pypi.org/p/${{ github.event.repository.name }} + permissions: + id-token: write + + steps: + - name: Download distributions + uses: actions/download-artifact@v4 + with: + name: python-package-distributions + path: dist/ + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c5328ae --- /dev/null +++ b/.gitignore @@ -0,0 +1,133 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +pip-wheel-metadata/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +.python-version + +# pipenv +Pipfile.lock + +# PEP 582 +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# IDEs +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# macOS +.DS_Store diff --git a/README.md b/README.md index 847260c..602f35f 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,181 @@ -## My Project +
+
+ + Strands Agents + +
+

Strands Agents Extension Template

+

Build and publish custom components for Strands Agents.

+

+ Documentation ◆ + Python SDK ◆ + Tools ◆ + Community Packages +

+
-TODO: Fill this README out! +This template helps you build and publish custom components for [Strands Agents](https://github.com/strands-agents/sdk-python). Whether you're creating a new tool, model provider, or session manager, this repo gives you a starting point with the right structure and conventions. -Be sure to: +## Getting started -* Change the title in this README -* Edit your repository description on GitHub +### 1. Create your repository + +Click "Use this template" on GitHub to create your own repository. Then clone it locally: + +```bash +git clone https://github.com/yourusername/your-repo-name +cd your-repo-name +``` + +### 2. Run the setup script + +The setup script customizes the template for your project. It renames files, updates imports, configures `pyproject.toml`, and removes components you don't need. + +```bash +python setup_template.py +``` + +You'll be prompted for: + +- **Package name** — A short identifier like `amazon`, `slack`, or `redis`. This becomes your module name (`strands_amazon`) and PyPI package name (`strands-amazon`). +- **Components** — Which extension points you want to include (tool, model, etc.) +- **Author info** — Your name, email, and GitHub username for `pyproject.toml`. +- **Description** — A one-line description of your package. + +### 3. Install dependencies + +```bash +pip install -e ".[dev]" +``` + +## What's in this template + +The template includes skeleton implementations for all major Strands extension points. + +| File | Component | Purpose | +|------|-----------|---------| +| `tool.py` | Tool | Add capabilities to agents using the `@tool` decorator | +| `model.py` | Model provider | Integrate custom LLM APIs | +| `plugin.py` | Plugin | Extend agent behavior with hooks and tools in a composable package | +| `session_manager.py` | Session manager | Persist conversations across restarts | +| `conversation_manager.py` | Conversation manager | Control context window and message history | + +The setup script will remove components you don't select, so you only keep what you need. + +## Implementing your components + +Each file contains a minimal skeleton. Here's what to implement: + +### Tools + +Tools let agents interact with external systems and perform actions. Implement your logic inside the decorated function and return a result dict. + +- [Creating custom tools](https://strandsagents.com/latest/user-guide/concepts/tools/custom-tools/) — Documentation +- [sleep](https://github.com/strands-agents/tools/blob/main/src/strands_tools/sleep.py) — Simple tool with error handling +- [browser](https://github.com/strands-agents/tools/blob/main/src/strands_tools/browser/__init__.py) — Multi-tool package example + +### Plugins + +Plugins provide a composable way to extend agent behavior by bundling hooks and tools into a single package. Use `@hook` to react to agent lifecycle events and `@tool` to add capabilities, all auto-discovered and registered when the plugin is attached to an agent. + +- [Plugins](https://strandsagents.com/latest/user-guide/concepts/plugins/) — Documentation +- [AgentSkills](https://github.com/strands-agents/sdk-python/tree/main/src/strands/vended_plugins/skills) — Plugin example with hooks and tools +- [Steering](https://github.com/strands-agents/sdk-python/tree/main/src/strands/vended_plugins/steering) — Advanced plugin example + +### Model providers + +Model providers connect agents to LLM APIs. Implement the `stream()` method to receive messages and yield streaming events. + +- [Custom providers](https://strandsagents.com/latest/user-guide/concepts/model-providers/custom_model_provider/) — Documentation +- [strands-clova](https://github.com/aidendef/strands-clova) — Community model provider example + +### Session managers + +Session managers persist conversations to external storage, enabling conversations to resume after restarts or be shared across instances. + +- [Session management](https://strandsagents.com/latest/user-guide/concepts/agents/session-management/) — Documentation +- [File session manager](https://github.com/strands-agents/sdk-python/blob/main/src/strands/session/file_session_manager.py) — Implementation example + +### Conversation managers + +Conversation managers control the context window and how message history grows over time. They handle trimming old messages or summarizing context to stay within model limits. + +- [Conversation management](https://strandsagents.com/latest/user-guide/concepts/agents/conversation-management/) — Documentation +- [Sliding window manager](https://github.com/strands-agents/sdk-python/blob/main/src/strands/agent/conversation_manager/sliding_window_conversation_manager.py) — Implementation example + +## Testing + +Run all checks (format, lint, typecheck, test): + +```bash +hatch run prepare +``` + +Or run them individually: + +```bash +hatch run test # Run tests +hatch run lint # Run linter +hatch run typecheck # Run type checker +hatch run format # Format code +``` + +## Publishing to PyPI + +You can publish manually or through GitHub Actions. + +### Option 1: GitHub release (recommended) + +The included workflow automatically publishes to PyPI when you create a GitHub release. Version is derived from the git tag automatically. + +1. Configure PyPI trusted publishing first (see below) +2. Create a release on GitHub with a tag like `v0.1.0` +3. The workflow runs checks, builds, and publishes + +To configure PyPI trusted publishing: + +1. Go to PyPI → Your projects → Publishing +2. Add a new pending publisher with your GitHub repo details +3. Set environment name to `pypi` + +**Note:** If you create a release without configuring trusted publishing, the workflow will fail. Set this up before your first release. + +### Option 2: Manual publish + +```bash +hatch build +pip install twine +twine upload dist/* +``` + +## Naming conventions + +Follow these conventions so your package fits the Strands ecosystem: + +| Item | Convention | Example | +|------|------------|---------| +| PyPI package | `strands-{name}` | `strands-amazon` | +| Python module | `strands_{name}` | `strands_amazon` | +| Model class | `{Name}Model` | `AmazonModel` | +| Plugin class | `{Name}Plugin` | `AmazonPlugin` | +| Session manager | `{Name}SessionManager` | `RedisSessionManager` | +| Conversation manager | `{Name}ConversationManager` | `SummarizingConversationManager` | +| Tool function | `{descriptive_name}` | `search_web`, `send_email` | + +## Get featured + +Help others discover your package by adding the `strands-agents` topic to your GitHub repository. This makes it easier for the community to find Strands extensions. + +To add topics: go to your repo → click the ⚙️ gear next to "About" → add `strands-agents` and other relevant topics. + +You can also submit your package to be featured on the Strands website. See [Get Featured](https://strandsagents.com/latest/community/get-featured/) for details. + +## Resources + +- [Strands Agents documentation](https://strandsagents.com/) +- [SDK Python repository](https://github.com/strands-agents/sdk-python) +- [Official tools repository](https://github.com/strands-agents/tools) +- [Community packages](https://strandsagents.com/latest/community/community-packages/) ## Security @@ -13,5 +183,4 @@ See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more inform ## License -This project is licensed under the Apache-2.0 License. - +Apache 2.0 — see [LICENSE](LICENSE) for details. diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..33a1481 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,89 @@ +[build-system] +requires = ["hatchling", "hatch-vcs"] +build-backend = "hatchling.build" + +[project] +name = "strands-template" +dynamic = ["version"] +description = "Your package description" +readme = "README.md" +requires-python = ">=3.10" +license = {text = "Apache-2.0"} +authors = [ + {name = "Your Name", email = "your.email@example.com"} +] +keywords = ["strands", "agents", "ai", "tool"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] + +dependencies = [ + "strands-agents>=1.0.0", + "pydantic>=2.0.0", +] + +[project.urls] +Homepage = "https://github.com/yourusername/strands-template" +Documentation = "https://github.com/yourusername/strands-template#readme" +Repository = "https://github.com/yourusername/strands-template" +Issues = "https://github.com/yourusername/strands-template/issues" + +[project.optional-dependencies] +dev = [ + "pytest>=8.0.0,<9.0.0", + "pytest-asyncio>=0.25.0,<1.0.0", + "ruff>=0.11.0,<1.0.0", + "mypy>=1.15.0,<2.0.0", + "hatch", +] + +[tool.hatch.version] +source = "vcs" + +[tool.hatch.build.targets.wheel] +packages = ["src/strands_template"] + +[tool.hatch.envs.default] +dependencies = [ + "pytest>=8.0.0,<9.0.0", + "pytest-asyncio>=0.25.0,<1.0.0", + "ruff>=0.11.0,<1.0.0", + "mypy>=1.15.0,<2.0.0", +] + +[tool.hatch.envs.default.scripts] +test = "pytest {args}" +lint = "ruff check src tests" +format = "ruff format src tests" +typecheck = "mypy src" +prepare = ["format", "lint", "typecheck", "test"] + +[tool.ruff] +line-length = 120 +include = ["src/**/*.py", "tests/**/*.py"] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle + "F", # pyflakes + "I", # isort + "B", # flake8-bugbear +] + +[tool.mypy] +python_version = "3.10" +warn_return_any = true +warn_unused_configs = true +ignore_missing_imports = true + +[tool.pytest.ini_options] +testpaths = ["tests"] +pythonpath = ["src"] +asyncio_mode = "auto" diff --git a/setup_template.py b/setup_template.py new file mode 100644 index 0000000..97efb1c --- /dev/null +++ b/setup_template.py @@ -0,0 +1,303 @@ +#!/usr/bin/env python3 +""" +Template Setup Script + +Run this after cloning the template to customize it for your project. +This script will: +1. Ask for your project details +2. Ask which components you want to keep +3. Rename files and directories +4. Replace placeholder values throughout the codebase +5. Delete unused components +6. Delete itself when done + +Usage: + python setup_template.py +""" + +import os +import re +import shutil +import sys + +COMPONENTS = { + "tool": { + "name": "Tool", + "description": "Add capabilities to agents using the @tool decorator", + "files": ["tool.py"], + "test_files": ["test_tool.py"], + "exports": ["template_tool"], + }, + "model": { + "name": "Model Provider", + "description": "Integrate custom LLM APIs", + "files": ["model.py"], + "test_files": ["test_model.py"], + "exports": ["TemplateModel"], + }, + "plugin": { + "name": "Plugin", + "description": "Extend agent behavior with hooks and tools in a composable package", + "files": ["plugin.py"], + "test_files": ["test_plugin.py"], + "exports": ["TemplatePlugin"], + }, + "session_manager": { + "name": "Session Manager", + "description": "Persist conversations across restarts", + "files": ["session_manager.py"], + "test_files": ["test_session_manager.py"], + "exports": ["TemplateSessionManager"], + }, + "conversation_manager": { + "name": "Conversation Manager", + "description": "Control context window and message history", + "files": ["conversation_manager.py"], + "test_files": ["test_conversation_manager.py"], + "exports": ["TemplateConversationManager"], + }, +} + + +def to_snake_case(name: str) -> str: + """Convert to snake_case (e.g., my_tool).""" + s = re.sub(r"[-\s]+", "_", name) + s = re.sub(r"([A-Z]+)([A-Z][a-z])", r"\1_\2", s) + s = re.sub(r"([a-z\d])([A-Z])", r"\1_\2", s) + return s.lower() + + +def to_pascal_case(name: str) -> str: + """Convert to PascalCase (e.g., MyTool).""" + words = re.split(r"[-_\s]+", name) + return "".join(word.capitalize() for word in words) + + +def to_kebab_case(name: str) -> str: + """Convert to kebab-case (e.g., my-tool).""" + s = to_snake_case(name) + return s.replace("_", "-") + + +def get_input(prompt: str, default: str = "") -> str: + """Get user input with optional default.""" + if default: + result = input(f"{prompt} [{default}]: ").strip() + return result if result else default + return input(f"{prompt}: ").strip() + + +def select_components() -> list[str]: + """Prompt user to select which components to keep.""" + print("\nWhich components do you want to include?\n") + + for i, (key, info) in enumerate(COMPONENTS.items(), 1): + print(f" {i}. {info['name']} - {info['description']}") + + print() + selection = get_input("Enter numbers separated by commas (e.g., 1,2)", "1") + + selected = [] + for num in selection.split(","): + num = num.strip() + if num.isdigit(): + idx = int(num) - 1 + if 0 <= idx < len(COMPONENTS): + selected.append(list(COMPONENTS.keys())[idx]) + + if not selected: + print("❌ No valid components selected") + sys.exit(1) + + return selected + + +def replace_in_file(filepath: str, replacements: dict[str, str]) -> None: + """Replace all occurrences in a file.""" + with open(filepath, "r", encoding="utf-8") as f: + content = f.read() + + for old, new in replacements.items(): + content = content.replace(old, new) + + with open(filepath, "w", encoding="utf-8") as f: + f.write(content) + + +def update_init_file(src_dir: str, selected: list[str], replacements: dict[str, str]) -> None: + """Update __init__.py to only export selected components.""" + init_path = os.path.join(src_dir, "__init__.py") + + imports = [] + exports = [] + + for key in selected: + info = COMPONENTS[key] + for export in info["exports"]: + # Apply replacements to get the new name + new_export = export + for old, new in replacements.items(): + new_export = new_export.replace(old, new) + + module = info["files"][0].replace(".py", "") + # Apply replacements to module name too + new_module = module + for old, new in replacements.items(): + new_module = new_module.replace(old, new) + + # Get the package name from src_dir (apply replacements for renamed package) + package_name = os.path.basename(src_dir) + for old, new in replacements.items(): + package_name = package_name.replace(old, new) + imports.append(f"from {package_name}.{new_module} import {new_export}") + exports.append(f' "{new_export}",') + + imports.sort() + exports.sort() + + content = f'''"""Strands Package.""" + +{chr(10).join(imports)} + +__all__ = [ +{chr(10).join(exports)} +] +''' + + with open(init_path, "w", encoding="utf-8") as f: + f.write(content) + + +def delete_unused_components(src_dir: str, selected: list[str]) -> None: + """Delete component files that weren't selected.""" + for key, info in COMPONENTS.items(): + if key not in selected: + # Delete source files + for filename in info["files"]: + filepath = os.path.join(src_dir, filename) + if os.path.exists(filepath): + os.remove(filepath) + print(f" ✓ Removed {filepath}") + + # Delete test files + for filename in info["test_files"]: + filepath = os.path.join("tests", filename) + if os.path.exists(filepath): + os.remove(filepath) + print(f" ✓ Removed {filepath}") + + +def main() -> None: + print("\n🔧 Strands Template Setup\n") + print("This will customize the template for your project.\n") + + # Gather information + package_name = get_input("Package name (e.g., 'google', 'aws', 'slack')") + if not package_name: + print("❌ Package name is required") + sys.exit(1) + + # Generate variations + snake_name = to_snake_case(package_name) + pascal_name = to_pascal_case(package_name) + kebab_name = to_kebab_case(package_name) + + print(f"\n PyPI package: strands-{kebab_name}") + print(f" Module: strands_{snake_name}") + print(f" Classes: {pascal_name}Model, {pascal_name}Hooks, etc.") + + # Select components + selected = select_components() + selected_names = [COMPONENTS[k]["name"] for k in selected] + print(f"\n Selected: {', '.join(selected_names)}") + + # Optional info + print() + author_name = get_input("Author name", "Your Name") + author_email = get_input("Author email", "your.email@example.com") + github_username = get_input("GitHub username", "yourusername") + description = get_input("Package description", f"Strands Agents components for {package_name}") + + # Confirm + print("\n" + "=" * 50) + confirm = get_input("\nProceed with setup? (y/n)", "y") + if confirm.lower() != "y": + print("Setup cancelled.") + sys.exit(0) + + print("\n⏳ Setting up project...\n") + + # Define replacements + replacements = { + # Package/module names + "strands-template": f"strands-{kebab_name}", + "strands_template": f"strands_{snake_name}", + # Class names + "TemplateModel": f"{pascal_name}Model", + "TemplatePlugin": f"{pascal_name}Plugin", + "TemplateSessionManager": f"{pascal_name}SessionManager", + "TemplateConversationManager": f"{pascal_name}ConversationManager", + # Function names + "template_tool": f"{snake_name}_tool", + # Plugin name + "template-plugin": f"{kebab_name}-plugin", + # Author info + "Your Name": author_name, + "your.email@example.com": author_email, + "yourusername": github_username, + "Your package description": description, + } + + # Determine which files to process based on selection + files_to_process = ["pyproject.toml", "README.md"] + + for key in selected: + info = COMPONENTS[key] + for filename in info["files"]: + files_to_process.append(f"src/strands_template/{filename}") + for filename in info["test_files"]: + files_to_process.append(f"tests/{filename}") + + # Always process __init__.py + files_to_process.append("src/strands_template/__init__.py") + + # Process files + for filepath in files_to_process: + if os.path.exists(filepath): + replace_in_file(filepath, replacements) + print(f" ✓ Updated {filepath}") + + # Delete unused components + print("\n🗑️ Removing unused components...") + delete_unused_components("src/strands_template", selected) + + # Update __init__.py with only selected exports + new_src = f"src/strands_{snake_name}" + update_init_file("src/strands_template", selected, replacements) + + # Rename source directory + old_src = "src/strands_template" + if os.path.exists(old_src) and old_src != new_src: + shutil.move(old_src, new_src) + print(f"\n ✓ Renamed {old_src} → {new_src}") + + # Clean up + print("\n🧹 Cleaning up...") + + # Remove this setup script + script_path = os.path.abspath(__file__) + if os.path.exists(script_path): + os.remove(script_path) + print(" ✓ Removed setup_template.py") + + print("\n✅ Setup complete!\n") + print("Next steps:") + print(" 1. Review the generated files") + print(" 2. Install dev dependencies: pip install -e '.[dev]'") + print(" 3. Run checks: hatch run prepare") + print(" 4. Start implementing your components") + print() + + +if __name__ == "__main__": + main() diff --git a/src/strands_template/__init__.py b/src/strands_template/__init__.py new file mode 100644 index 0000000..2590ba0 --- /dev/null +++ b/src/strands_template/__init__.py @@ -0,0 +1,15 @@ +"""Strands Template Package.""" + +from strands_template.conversation_manager import TemplateConversationManager +from strands_template.model import TemplateModel +from strands_template.plugin import TemplatePlugin +from strands_template.session_manager import TemplateSessionManager +from strands_template.tool import template_tool + +__all__ = [ + "template_tool", + "TemplateModel", + "TemplatePlugin", + "TemplateSessionManager", + "TemplateConversationManager", +] diff --git a/src/strands_template/conversation_manager.py b/src/strands_template/conversation_manager.py new file mode 100644 index 0000000..331f79f --- /dev/null +++ b/src/strands_template/conversation_manager.py @@ -0,0 +1,27 @@ +"""Conversation Manager Implementation.""" + +import logging +from typing import TYPE_CHECKING, Any + +from strands.agent.conversation_manager.conversation_manager import ConversationManager + +if TYPE_CHECKING: + from strands.agent.agent import Agent + +logger = logging.getLogger(__name__) + + +class TemplateConversationManager(ConversationManager): + """Template conversation manager implementation.""" + + def __init__(self) -> None: + """Initialize the conversation manager.""" + super().__init__() + + def apply_management(self, agent: "Agent", **kwargs: Any) -> None: + """Apply conversation management strategy.""" + pass + + def reduce_context(self, agent: "Agent", e: Exception | None = None, **kwargs: Any) -> None: + """Reduce conversation context when overflow occurs.""" + pass diff --git a/src/strands_template/model.py b/src/strands_template/model.py new file mode 100644 index 0000000..a288da6 --- /dev/null +++ b/src/strands_template/model.py @@ -0,0 +1,61 @@ +"""Model Provider Implementation.""" + +import logging +from collections.abc import AsyncGenerator, AsyncIterable +from typing import Any, TypeVar + +from pydantic import BaseModel +from strands.models.model import Model +from strands.types.content import Messages +from strands.types.streaming import StreamEvent +from strands.types.tools import ToolSpec +from typing_extensions import override + +logger = logging.getLogger(__name__) + +T = TypeVar("T", bound=BaseModel) + + +class TemplateModel(Model): + """Template model provider implementation.""" + + def __init__(self, api_key: str, model_id: str) -> None: + """Initialize the model provider.""" + self.api_key = api_key + self.model_id = model_id + + @override + def update_config(self, **model_config: Any) -> None: + """Update the model configuration.""" + pass + + @override + def get_config(self) -> dict[str, Any]: + """Get the current model configuration.""" + return {"api_key": self.api_key, "model_id": self.model_id} + + @override + async def stream( + self, + messages: Messages, + tool_specs: list[ToolSpec] | None = None, + system_prompt: str | None = None, + **kwargs: Any, + ) -> AsyncIterable[StreamEvent]: + """Stream conversation with the model.""" + # TODO: Implement streaming logic + raise NotImplementedError + yield # type: ignore + + @override + async def structured_output( + self, + output_model: type[T], + prompt: Messages, + system_prompt: str | None = None, + **kwargs: Any, + ) -> AsyncGenerator[dict[str, T | Any], None]: + """Generate structured output from the model.""" + # TODO: Implement structured output logic + raise NotImplementedError + yield # type: ignore diff --git a/src/strands_template/plugin.py b/src/strands_template/plugin.py new file mode 100644 index 0000000..469ade2 --- /dev/null +++ b/src/strands_template/plugin.py @@ -0,0 +1,68 @@ +"""Plugin Implementation.""" + +import logging +from typing import TYPE_CHECKING + +from strands import tool +from strands.hooks import BeforeToolCallEvent +from strands.plugins import Plugin, hook + +if TYPE_CHECKING: + from strands.agent.agent import Agent + +logger = logging.getLogger(__name__) + + +class TemplatePlugin(Plugin): + """Template plugin implementation. + + Plugins provide a composable way to extend agent behavior through + automatic hook and tool registration. Methods decorated with @hook + and @tool are discovered and registered automatically. + + Example: + ```python + from strands import Agent + from strands_template import TemplatePlugin + + plugin = TemplatePlugin() + agent = Agent(plugins=[plugin]) + ``` + """ + + name = "template-plugin" + + def __init__(self) -> None: + """Initialize the plugin.""" + super().__init__() + + def init_agent(self, agent: "Agent") -> None: + """Initialize the plugin with an agent instance. + + Decorated hooks and tools are auto-registered by the plugin registry. + Override this method to add custom initialization logic. + + Args: + agent: The agent instance to extend. + """ + pass + + @hook # type: ignore[call-overload] + def on_before_tool_call(self, event: BeforeToolCallEvent) -> None: + """Hook that runs before each tool call. + + Args: + event: The before-tool-call event with tool_use and agent reference. + """ + # TODO: Implement your hook logic + pass + + @tool + def template_plugin_tool(self, param1: str) -> str: + """Brief description of what your plugin tool does. + + Args: + param1: Description of parameter 1. + """ + # TODO: Implement your tool logic + raise NotImplementedError diff --git a/src/strands_template/session_manager.py b/src/strands_template/session_manager.py new file mode 100644 index 0000000..4770490 --- /dev/null +++ b/src/strands_template/session_manager.py @@ -0,0 +1,36 @@ +"""Session Manager Implementation.""" + +import logging +from typing import TYPE_CHECKING, Any + +from strands.session.session_manager import SessionManager +from strands.types.content import Message + +if TYPE_CHECKING: + from strands.agent.agent import Agent + +logger = logging.getLogger(__name__) + + +class TemplateSessionManager(SessionManager): + """Template session manager implementation.""" + + def __init__(self, session_id: str) -> None: + """Initialize the session manager.""" + self.session_id = session_id + + def initialize(self, agent: "Agent", **kwargs: Any) -> None: + """Initialize an agent with session data.""" + pass + + def append_message(self, message: Message, agent: "Agent", **kwargs: Any) -> None: + """Append a message to the session storage.""" + pass + + def redact_latest_message(self, redact_message: Message, agent: "Agent", **kwargs: Any) -> None: + """Redact (replace) the most recently appended message.""" + pass + + def sync_agent(self, agent: "Agent", **kwargs: Any) -> None: + """Synchronize agent state with session storage.""" + pass diff --git a/src/strands_template/tool.py b/src/strands_template/tool.py new file mode 100644 index 0000000..eb55d43 --- /dev/null +++ b/src/strands_template/tool.py @@ -0,0 +1,22 @@ +"""Tool Implementation.""" + +import logging +from typing import Any + +from strands import tool + +logger = logging.getLogger(__name__) + + +@tool +def template_tool(param1: str) -> dict[str, Any]: + """Brief description of what your tool does. + + Args: + param1: Description of parameter 1. + + Returns: + Dict containing status and response content. + """ + # TODO: Implement your tool logic + raise NotImplementedError diff --git a/tests/test_conversation_manager.py b/tests/test_conversation_manager.py new file mode 100644 index 0000000..84a9b21 --- /dev/null +++ b/tests/test_conversation_manager.py @@ -0,0 +1,9 @@ +"""Tests for TemplateConversationManager.""" + +from strands_template import TemplateConversationManager + + +def test_template_conversation_manager_init(): + """Test initialization.""" + cm = TemplateConversationManager() + assert cm is not None diff --git a/tests/test_model.py b/tests/test_model.py new file mode 100644 index 0000000..8fe7411 --- /dev/null +++ b/tests/test_model.py @@ -0,0 +1,11 @@ +"""Tests for TemplateModel.""" + +from strands_template import TemplateModel + + +def test_template_model_init(): + """Test initialization.""" + model = TemplateModel(api_key="test-key", model_id="test-model") + config = model.get_config() + assert config["api_key"] == "test-key" + assert config["model_id"] == "test-model" diff --git a/tests/test_plugin.py b/tests/test_plugin.py new file mode 100644 index 0000000..34047ee --- /dev/null +++ b/tests/test_plugin.py @@ -0,0 +1,9 @@ +"""Tests for TemplatePlugin.""" + +from strands_template import TemplatePlugin + + +def test_template_plugin_init(): + """Test initialization.""" + plugin = TemplatePlugin() + assert plugin.name is not None diff --git a/tests/test_session_manager.py b/tests/test_session_manager.py new file mode 100644 index 0000000..d60c225 --- /dev/null +++ b/tests/test_session_manager.py @@ -0,0 +1,9 @@ +"""Tests for TemplateSessionManager.""" + +from strands_template import TemplateSessionManager + + +def test_template_session_manager_init(): + """Test initialization.""" + session = TemplateSessionManager(session_id="test-session") + assert session.session_id == "test-session" diff --git a/tests/test_tool.py b/tests/test_tool.py new file mode 100644 index 0000000..3425d5a --- /dev/null +++ b/tests/test_tool.py @@ -0,0 +1,7 @@ +"""Tests for template_tool.""" + + +def test_template_tool(): + """Test basic functionality.""" + # TODO: Implement tests + ...