diff --git a/.env.example b/.env.example index 3153a87..986caa5 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,9 @@ GOOGLE_API_KEY="your_gemini_key_here" ANTHROPIC_API_KEY="sk-ant-..." OPENAI_API_KEY="sk-..." DEEPSEEK_API_KEY="your_deepseek_key_here" +# Optional — OpenAI-compatible hosts (e.g. examples/openai_compatible_host.py via Groq) +# GROQ_API_KEY="your_groq_key_here" +# GROQ_MODEL="llama-3.3-70b-versatile" # GitHub (Optional — used by dev_tools/issue_resolver) # Without this key the GitHub API applies an unauthenticated rate limit (60 req/hr per IP). diff --git a/CHANGELOG.md b/CHANGELOG.md index d4d9950..32a04f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Contributors add user-facing entries under `[Unreleased]` in the same PR. Mainta ### Added +- **Documentation:** OpenAI-compatible host guide and runnable Groq example covering shared `to_openai_tool()` usage, provider base URLs, credentials, and local servers (#261). - **Framework:** `skillware/core/ui_schema.py` helpers to resolve dot paths and validate output-card field keys (#199). - **CI:** Parametrized guard `tests/test_card_ui_schema.py` — every registry skill with output-card `ui_schema` must ship a fixture under `tests/fixtures/card_ui_schema/` whose samples cover all field keys (#199). diff --git a/README.md b/README.md index fca645c..c22f2f2 100644 --- a/README.md +++ b/README.md @@ -216,14 +216,14 @@ for part in response.candidates[0].content.parts: print(part.text) ``` -For other providers and shared integration patterns, see the [usage guides index](docs/usage/README.md), [agent loops](docs/usage/agent_loops.md), [Gemini](docs/usage/gemini.md), [Claude](docs/usage/claude.md), [OpenAI](docs/usage/openai.md), [DeepSeek](docs/usage/deepseek.md), [Ollama](docs/usage/ollama.md), [API keys for skills](docs/usage/api_keys.md), and the [skill usage template](docs/usage/skill_usage_template.md) for contributors. +For other providers and shared integration patterns, see the [usage guides index](docs/usage/README.md), [agent loops](docs/usage/agent_loops.md), [Gemini](docs/usage/gemini.md), [Claude](docs/usage/claude.md), [OpenAI](docs/usage/openai.md), [OpenAI-compatible hosts](docs/usage/openai_compatible.md), [DeepSeek](docs/usage/deepseek.md), [Ollama](docs/usage/ollama.md), [API keys for skills](docs/usage/api_keys.md), and the [skill usage template](docs/usage/skill_usage_template.md) for contributors. ## Documentation | Topic | Links | | :--- | :--- | | **Introduction** | [Introduction](docs/introduction.md) · [Vision](docs/vision.md) · [Comparison](COMPARISON.md) | -| **Usage guides** | [Skill Library](docs/skills/README.md) · [Usage Guide](docs/usage/README.md) · [Install extras](docs/usage/install_extras.md) · [Examples](examples/README.md) · [Agent Loops](docs/usage/agent_loops.md) · [API Keys](docs/usage/api_keys.md) · [CLI](docs/usage/cli.md) | +| **Usage guides** | [Skill Library](docs/skills/README.md) · [Usage Guide](docs/usage/README.md) · [OpenAI-compatible hosts](docs/usage/openai_compatible.md) · [Install extras](docs/usage/install_extras.md) · [Examples](examples/README.md) · [Agent Loops](docs/usage/agent_loops.md) · [API Keys](docs/usage/api_keys.md) · [CLI](docs/usage/cli.md) | | **Security** | [Skill trust model](docs/security/skill-trust-model.md) · [SECURITY.md](SECURITY.md) | | **Contributing** | [Contributing](CONTRIBUTING.md) · [Agent Native Workflow](docs/contributing/ai_native_workflow.md) · [Testing](docs/TESTING.md) · [Changelog](CHANGELOG.md) | diff --git a/docs/usage/README.md b/docs/usage/README.md index a5f82c1..7f39473 100644 --- a/docs/usage/README.md +++ b/docs/usage/README.md @@ -21,6 +21,7 @@ To list locally available skills, inspect path resolution, or run bundle tests f | Google Gemini | `to_gemini_tool()` | [gemini.md](gemini.md) | `GOOGLE_API_KEY` (install `skillware[gemini]` for `google-genai`) | | Anthropic Claude | `to_claude_tool()` | [claude.md](claude.md) | `ANTHROPIC_API_KEY` | | OpenAI (ChatGPT) | `to_openai_tool()` | [openai.md](openai.md) | `OPENAI_API_KEY` | +| OpenAI-compatible hosts | `to_openai_tool()` | [openai_compatible.md](openai_compatible.md) | Host-specific key | | DeepSeek | `to_deepseek_tool()` | [deepseek.md](deepseek.md) | `DEEPSEEK_API_KEY` | | Ollama (prompt mode) | `to_ollama_prompt()` | [ollama.md](ollama.md) | (local; no cloud key) | | CLI | `skillware list`, `skillware paths`, `skillware test`, `skillware examples` | [cli.md](cli.md) | pytest in `[dev]` for `test` | diff --git a/docs/usage/agent_loops.md b/docs/usage/agent_loops.md index 1663f8f..8f699ca 100644 --- a/docs/usage/agent_loops.md +++ b/docs/usage/agent_loops.md @@ -42,6 +42,8 @@ flowchart LR Provider guides contain full API details. Skill pages contain copy-paste examples with skill-specific paths and sample user messages. +OpenAI-compatible hosts reuse `to_openai_tool()`; see the [host guide](openai_compatible.md) and runnable [Groq example](../../examples/openai_compatible_host.py). + **Optional param validation:** Some agent-loop examples (e.g. `claude_wallet_check.py`, `gemini_tos_evaluator.py`) call `skill.validate_params(...)` before `execute()`; others call `execute()` directly. --- @@ -105,4 +107,3 @@ skills in one harness. | `defi/evm_tx_handler` | - | `gemini_evm_tx_handler.py` | `claude_evm_tx_handler.py` | - | - | - | | `monitoring/token_limiter` | `token_limiter_loop.py` (local execute) | `gemini_token_limiter.py` | `claude_token_limiter.py` | (catalog page) | (catalog page) | (catalog page) | | `finance/uk_companies_house_handler` | `uk_companies_house_handler_demo.py` | `gemini_uk_companies_house_handler.py` | (catalog page) | (catalog page) | (catalog page) | (catalog page) | - diff --git a/docs/usage/api_keys.md b/docs/usage/api_keys.md index 3c8e144..8b0c7d6 100644 --- a/docs/usage/api_keys.md +++ b/docs/usage/api_keys.md @@ -177,5 +177,6 @@ Skills that talk only to `localhost` (for example a local Ollama instance) may n - [Usage: Gemini](gemini.md) — agent-side `GOOGLE_API_KEY` - [Usage: Claude](claude.md) — agent-side `ANTHROPIC_API_KEY` - [Usage: OpenAI](openai.md) — agent-side `OPENAI_API_KEY` +- [OpenAI-compatible model hosts](openai_compatible.md) — host-specific keys and base URLs - [Usage: DeepSeek](deepseek.md) — agent-side `DEEPSEEK_API_KEY` - [Skill library](../skills/README.md) — per-skill environment requirements diff --git a/docs/usage/openai.md b/docs/usage/openai.md index 72b2962..826fc4d 100644 --- a/docs/usage/openai.md +++ b/docs/usage/openai.md @@ -4,6 +4,10 @@ Skillware supports OpenAI Chat Completions tool calling via `SkillLoader.to_open For agent credentials, set `OPENAI_API_KEY` (see [API keys for skills](api_keys.md) for local and CI setup). Skills that call external APIs during `execute()` may require additional variables documented on each skill page. +The same adapter also works with model hosts that accept the OpenAI `tools` +schema. See [OpenAI-compatible model hosts](openai_compatible.md) for provider +base URLs, credentials, and compatibility boundaries. + --- ## Quick snippet @@ -22,6 +26,18 @@ client = OpenAI() # Pass bundle["instructions"] as the system message when you start the chat. ``` +For a compatible host, keep the same adapter and configure the client. For +example, Groq uses a different key and base URL: + +```python +import os + +client = OpenAI( + api_key=os.environ["GROQ_API_KEY"], + base_url="https://api.groq.com/openai/v1", +) +``` + --- ## How it works @@ -133,6 +149,7 @@ print(response.choices[0].message.content) ## Related documents +- [OpenAI-compatible model hosts](openai_compatible.md) - [API keys for skills](api_keys.md) - [Usage: DeepSeek](deepseek.md) (separate adapter) - [Usage: Gemini](gemini.md) diff --git a/docs/usage/openai_compatible.md b/docs/usage/openai_compatible.md new file mode 100644 index 0000000..3ba80a3 --- /dev/null +++ b/docs/usage/openai_compatible.md @@ -0,0 +1,99 @@ +# OpenAI-compatible model hosts + +Many model hosts accept the OpenAI Chat Completions `tools` schema. With those +hosts, use `SkillLoader.to_openai_tool()` and configure the OpenAI client with +the host's API key and `base_url`. A separate Skillware adapter is not needed +just because the model or vendor name is different. + +Compatibility is host- and model-specific. Before relying on a model, confirm +that its current documentation says it supports Chat Completions tool calling, +then use the model ID published by that host. + +## Choose the adapter + +| Situation | Skillware adapter | Guide | +| :--- | :--- | :--- | +| OpenAI or a host that accepts the OpenAI `tools` schema | `to_openai_tool()` | This guide and [OpenAI](openai.md) | +| DeepSeek's first-party API | `to_deepseek_tool()` | [DeepSeek](deepseek.md) | +| Gemini or Claude's native tool API | `to_gemini_tool()` or `to_claude_tool()` | [Gemini](gemini.md) or [Claude](claude.md) | +| Ollama prompt mode, without OpenAI-style tool calling | `to_ollama_prompt()` | [Ollama](ollama.md) | + +Use a provider-specific adapter when the provider's schema differs. Do not add +one `to_*_tool()` method per OpenAI-compatible vendor. + +## Client pattern + +This Groq example changes only the credential, base URL, and model passed to +the standard OpenAI client: + +```python +import os + +from openai import OpenAI + +from skillware.core.env import load_env_file +from skillware.core.loader import SkillLoader + +load_env_file() + +bundle = SkillLoader.load_skill("compliance/tos_evaluator") +tool = SkillLoader.to_openai_tool(bundle) + +client = OpenAI( + api_key=os.environ["GROQ_API_KEY"], + base_url="https://api.groq.com/openai/v1", +) + +response = client.chat.completions.create( + model="", + messages=[ + {"role": "system", "content": bundle["instructions"]}, + {"role": "user", "content": "Check https://example.com/docs."}, + ], + tools=[tool], +) +``` + +Handle `response.choices[0].message.tool_calls`, execute the skill, and return +the result exactly as in the [shared agent loop](agent_loops.md). + +For a complete runnable Groq loop (load skill, call host, `execute()`, return +tool result), see [`examples/openai_compatible_host.py`](../../examples/openai_compatible_host.py). + +## Common hosts + +The endpoints below are the providers' documented general OpenAI-compatible +base URLs. Provider plans, regional endpoints, model IDs, and feature support +can change, so follow the linked provider documentation for current details. + +| Host | `base_url` | Typical key variable | Notes | +| :--- | :--- | :--- | :--- | +| [Kimi / Moonshot](https://platform.kimi.ai/docs/api/chat) | `https://api.moonshot.ai/v1` | `MOONSHOT_API_KEY` | Chat Completions supports function tools; select a current Kimi model with tool use. | +| [Z.AI GLM](https://docs.z.ai/guides/develop/openai/python) | `https://api.z.ai/api/paas/v4/` | `ZAI_API_KEY` | Use the general API endpoint; the Coding Plan has a separate endpoint and use case. | +| [Groq](https://console.groq.com/docs/openai) | `https://api.groq.com/openai/v1` | `GROQ_API_KEY` | Use a model that the Groq model catalog marks for tool use. | +| [Mistral](https://docs.mistral.ai/resources/migration-guides) | `https://api.mistral.ai/v1` | `MISTRAL_API_KEY` | The OpenAI-compatible path uses Mistral model IDs. | +| [Together AI](https://docs.together.ai/docs/inference/openai-compatibility) | `https://api.together.ai/v1` | `TOGETHER_API_KEY` | Model IDs commonly use a `provider/model` form. | +| [Fireworks AI](https://docs.fireworks.ai/tools-sdks/openai-compatibility) | `https://api.fireworks.ai/inference/v1` | `FIREWORKS_API_KEY` | Use a Fireworks model or deployment ID that supports tools. | +| [OpenRouter](https://openrouter.ai/docs/quickstart) | `https://openrouter.ai/api/v1` | `OPENROUTER_API_KEY` | This is a multi-model gateway; tool support depends on the routed model and provider. | +| [vLLM](https://docs.vllm.ai/en/latest/serving/online_serving/openai_compatible_server/) | `http://localhost:8000/v1` | Operator-defined | The server may require a configured token; the served model must support an appropriate chat template and tools. | +| [LiteLLM proxy](https://docs.litellm.ai/) | `http://localhost:4000` | Operator-defined | Use the proxy key and model alias configured by the proxy operator. | + +Never commit API keys. Load them from the environment as described in +[API keys for skills](api_keys.md). + +## Local-server boundary + +An OpenAI-compatible route does not guarantee compatible tool behavior. For a +local vLLM or LiteLLM deployment, verify the server's tool-calling configuration +and the selected model before sending `tools=[tool]`. If the local runtime only +supports an instruction prompt, use the [Ollama prompt-mode guide](ollama.md) +instead of treating it as structured tool calling. + +## Related documents + +- [Runnable example: `openai_compatible_host.py`](../../examples/openai_compatible_host.py) +- [Usage: OpenAI](openai.md) +- [Usage: DeepSeek](deepseek.md) +- [Shared agent loops](agent_loops.md) +- [API keys for skills](api_keys.md) +- [Usage guides index](README.md) diff --git a/examples/README.md b/examples/README.md index a7b2615..6bdbf38 100644 --- a/examples/README.md +++ b/examples/README.md @@ -14,6 +14,7 @@ from GitHub (network required); see - [Gemini](../docs/usage/gemini.md) - [Claude](../docs/usage/claude.md) - [OpenAI](../docs/usage/openai.md) +- [OpenAI-compatible hosts](../docs/usage/openai_compatible.md) - [DeepSeek](../docs/usage/deepseek.md) - [Ollama](../docs/usage/ollama.md) - [Install extras](../docs/usage/install_extras.md) @@ -53,6 +54,7 @@ pip install -e ".[dev,all,agents]" | `ollama_skills_test.py` | `finance/wallet_screening`, `office/pdf_form_filler`, `optimization/prompt_rewriter` | Ollama | `[finance_wallet_screening]`, `[office_pdf_form_filler]`, `[optimization_prompt_rewriter]`; install `ollama` separately | `ETHERSCAN_API_KEY`, `ANTHROPIC_API_KEY` | Loads multiple skills and tests prompt-mode tool calling with Ollama. | | `ollama_tos_evaluator.py` | `compliance/tos_evaluator` | Ollama | `[compliance_tos_evaluator]`; install `ollama` separately | None | Runs the terms-of-service evaluator with local Ollama prompt-mode calls. | | `openai_tos_evaluator.py` | `compliance/tos_evaluator` | OpenAI | `[compliance_tos_evaluator]`, `[openai]` | `OPENAI_API_KEY` | Runs the terms-of-service evaluator with OpenAI function calling. | +| `openai_compatible_host.py` | `compliance/tos_evaluator` | Groq (OpenAI-compatible) | `[compliance_tos_evaluator]`, `[openai]` | `GROQ_API_KEY`, `GROQ_MODEL` | Runs the terms-of-service evaluator through Groq's OpenAI-compatible API. | | `pii_guardrail_flow.py` | `compliance/pii_masker` | Local execute | `[compliance_pii_masker]` | None | Demonstrates local PII masking before passing text to an external agent. | | `prompt_compression_demo.py` | `optimization/prompt_rewriter` | Local execute | `[optimization_prompt_rewriter]` | None | Demonstrates prompt compression without a provider loop. | | `novelty_extractor_demo.py` | `data_engineering/novelty_extractor` | Local execute | `[data_engineering_novelty_extractor]` | None | Demonstrates multi-turn corpus distillation using local embeddings with no API key. | diff --git a/examples/openai_compatible_host.py b/examples/openai_compatible_host.py new file mode 100644 index 0000000..cb4688a --- /dev/null +++ b/examples/openai_compatible_host.py @@ -0,0 +1,74 @@ +"""Run a Skillware tool loop through the OpenAI-compatible Groq API.""" + +import json +import os + +from openai import OpenAI + +from skillware.core.env import load_env_file +from skillware.core.loader import SkillLoader + +load_env_file() + +bundle = SkillLoader.load_skill("compliance/tos_evaluator") +print(f"Loaded Skill: {bundle['manifest']['name']}") + +tos_skill = bundle["class"]() +openai_tool = SkillLoader.to_openai_tool(bundle) +tool_name = openai_tool["function"]["name"] +print(f"OpenAI tool name: {tool_name}") + +client = OpenAI( + api_key=os.environ["GROQ_API_KEY"], + base_url="https://api.groq.com/openai/v1", +) +model = os.environ["GROQ_MODEL"] + +user_query = ( + "Before an agent crawls https://hackernoon.com/tagged/ai for research, " + "check whether that appears allowed." +) +print(f"User: {user_query}") + +messages = [ + {"role": "system", "content": bundle["instructions"]}, + {"role": "user", "content": user_query}, +] + +response = client.chat.completions.create( + model=model, + messages=messages, + tools=[openai_tool], +) + +while response.choices[0].message.tool_calls: + assistant_message = response.choices[0].message + messages.append(assistant_message) + + for tool_call in assistant_message.tool_calls: + if tool_call.function.name != tool_name: + raise RuntimeError(f"Unexpected tool: {tool_call.function.name}") + + fn_args = json.loads(tool_call.function.arguments) + print(f"OpenAI-compatible host requested tool: {tool_call.function.name}") + print(f"Input: {fn_args}") + + result = tos_skill.execute(fn_args) + print(json.dumps(result, indent=2)) + + messages.append( + { + "role": "tool", + "tool_call_id": tool_call.id, + "content": json.dumps(result), + } + ) + + response = client.chat.completions.create( + model=model, + messages=messages, + tools=[openai_tool], + ) + +print("\nFinal Response:") +print(response.choices[0].message.content or "")