Skill: ayflying/ollama-2 (version 0.1.0)

--- name: ollama description: | Ollama 本地大模型调用技能。支持通过 API 与 Ollama 实例交互进行文本生成。 Use when: (1) 需要调用本地或远程 Ollama 模型 (2) 需要执行 LLM 推理任务 (3) 需要通过 Python 脚本与特定 Ollama 实例 (如 qwen3.5:9b) 交互。 --- # Ollama 技能 通过 API 方式调用 Ollama 模型。支持自定义 Host、模型名称以及 Prompt 提示词。 ## 核心配置 - **环境变量**: 见 `.env.example` - **默认地址**: `http://100.66.1.2:11434` - **默认模型**: `qwen3.5:9b` (推荐) ## 使用方法 ### 调用示例 ```bash # 基本调用（使用默认模型） python scripts/ollama_query.py "请简述量子力学" # 指定模型调用 python scripts/ollama_query.py "写一首关于春天的诗" qwen2.5:7b ``` ## 安装说明 使用 `npx skills` 工具安装： ```bash npx skills add ayflying/ai-skills --skill ollama ``` ## 文件结构 ``` ollama/ ├── SKILL.md # 技能说明 ├── scripts/ │ ├── ollama_query.py # 模型查询核心脚本 │ └── test_ollama.py # 连通性测试脚本 ├── .env.example # 环境变量模板 └── assets/ # 资源目录 ``` ## 注意事项 1. **环境准备**: 确保本地已安装 Ollama 且服务已启动 (`ollama serve`)。 2. **模型下载**: 使用前需先下载模型，例如 `ollama pull qwen3.5:9b`。 3. **Python 依赖**: 需要安装 `requests` 库 (`pip install requests`)。

path: scripts/ollama_query.py
content: import os
import sys
import json
import logging
from typing import Generator, Optional, Union

import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry

# 尝试加载 .env 文件 (符合项目已有的 python-dotenv 依赖)
try:
    from dotenv import load_dotenv

    load_dotenv()
except ImportError:
    pass

# 配置项，支持从环境变量或 .env 读取
OLLAMA_HOST = os.environ.get("OLLAMA_HOST", "http://100.66.1.2:11434").rstrip("/")
OLLAMA_MODEL = os.environ.get("OLLAMA_MODEL", "qwen3.5:9b")

# 日志配置
logging.basicConfig(level=logging.INFO, format="%(levelname)s: %(message)s")
logger = logging.getLogger(__name__)


def create_session(retries: int = 3, backoff_factor: float = 0.3) -> requests.Session:
    """创建带有重试策略的请求 Session"""
    session = requests.Session()
    retry_strategy = Retry(
        total=retries,
        backoff_factor=backoff_factor,
        status_forcelist=[429, 500, 502, 503, 504],
        allowed_methods=["POST"],
    )
    adapter = HTTPAdapter(max_retries=retry_strategy)
    session.mount("http://", adapter)
    session.mount("https://", adapter)
    return session


def query_ollama(
    prompt: str, model: str = OLLAMA_MODEL, stream: bool = False, timeout: int = 120
) -> Union[str, Generator[str, None, None]]:
    """
    带重试和增强异常处理的 Ollama 查询函数
    """
    url = f"{OLLAMA_HOST}/api/generate"
    payload = {"model": model, "prompt": prompt, "stream": stream}

    session = create_session()

    try:
        # 将超时时间提高到 120s 以应对复杂推理
        response = session.post(url, json=payload, timeout=timeout)
        response.raise_for_status()

        if stream:
            return _handle_stream(response)
        else:
            try:
                result = response.json()
                return result.get("response", "")
            except json.JSONDecodeError:
                return f"Error: 无法从 {url} 解析 JSON 响应"

    except requests.exceptions.RequestException as e:
        logger.error(f"连接 Ollama 失败: {e}")
        return f"Error: {str(e)}"


def _handle_stream(response: requests.Response) -> Generator[str, None, None]:
    """流式响应处理辅助函数"""
    for line in response.iter_lines():
        if not line:
            continue
        try:
            chunk = json.loads(line)
            if not chunk.get("done"):
                yield chunk.get("response", "")
        except json.JSONDecodeError:
            logger.warning("跳过一个格式错误的流数据块")
            continue


if __name__ == "__main__":
    if len(sys.argv) < 2:
        print('用法: python ollama_query.py "prompt" [model]')
        sys.exit(1)

    user_prompt = sys.argv[1]
    target_model = sys.argv[2] if len(sys.argv) > 2 else OLLAMA_MODEL

    # 执行非流式查询并输出
    result = query_ollama(user_prompt, target_model, stream=False)

    if isinstance(result, str):
        print(result)
    else:
        # 如果后续需要在命令行启用流式，此逻辑可处理生成器
        for text in result:
            print(text, end="", flush=True)


path: scripts/test_ollama.py
content: import unittest
from unittest.mock import patch, MagicMock
import requests
import json
import io
import logging

# 禁用测试期间的日志输出，使结果更整洁
logging.disable(logging.CRITICAL)

# 导入要测试的函数
from ollama_query import query_ollama


class TestOllamaQuery(unittest.TestCase):
    @patch("requests.Session.post")
    def test_query_ollama_success(self, mock_post):
        """测试正常响应（非流式）"""
        # 模拟响应对象
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.json.return_value = {"response": "Hello world", "done": True}
        mock_post.return_value = mock_response

        result = query_ollama("hi", model="test-model", stream=False)

        self.assertEqual(result, "Hello world")
        mock_post.assert_called_once()
        # 验证发送的参数 (第一个参数是 url)
        args, kwargs = mock_post.call_args
        self.assertIn("/api/generate", args[0])
        self.assertEqual(kwargs["json"]["model"], "test-model")
        self.assertEqual(kwargs["json"]["prompt"], "hi")
        self.assertFalse(kwargs["json"]["stream"])

    @patch("requests.Session.post")
    def test_query_ollama_streaming_success(self, mock_post):
        """测试正常响应（流式）"""
        # 模拟流式响应内容
        chunks = [
            json.dumps({"response": "Hello", "done": False}),
            json.dumps({"response": " world", "done": False}),
            json.dumps({"done": True}),
        ]

        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_response.iter_lines.return_value = [c.encode("utf-8") for c in chunks]
        mock_post.return_value = mock_response

        # query_ollama 在 stream=True 时是一个生成器
        gen = query_ollama("hi", stream=True)
        results = list(gen)

        self.assertEqual(results, ["Hello", " world"])
        mock_post.assert_called_once()
        self.assertTrue(mock_post.call_args.kwargs["json"]["stream"])

    @patch("requests.Session.post")
    def test_query_ollama_network_error(self, mock_post):
        """测试网络连接错误"""
        mock_post.side_effect = requests.exceptions.ConnectionError("Connection failed")

        result = query_ollama("hi")

        self.assertIn("Error", result)
        self.assertIn("Connection failed", result)

    @patch("requests.Session.post")
    def test_query_ollama_http_error(self, mock_post):
        """测试 HTTP 错误（如 404 模型不存在）"""
        mock_response = MagicMock()
        mock_response.status_code = 404
        mock_response.raise_for_status.side_effect = requests.exceptions.HTTPError(
            "404 Client Error: Not Found"
        )
        mock_post.return_value = mock_response

        result = query_ollama("hi", model="non-existent-model")

        self.assertIn("Error", result)
        self.assertIn("404 Client Error", result)

    @patch("requests.Session.post")
    def test_query_ollama_invalid_url(self, mock_post):
        """测试异常 URL 情况"""
        mock_post.side_effect = requests.exceptions.MissingSchema("Invalid URL")

        with patch("ollama_query.OLLAMA_HOST", "invalid_host"):
            result = query_ollama("hi")
            self.assertIn("Error", result)
            self.assertIn("Invalid URL", result)


if __name__ == "__main__":
    unittest.main(buffer=True)


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: clean (LOW)