add-llm-client

GitHub

指导如何在 kgb/clients 模块中添加新的 LLM 客户端提供商,涵盖工厂模式注册、抽象基类实现、配置结构及依赖管理,支持接入各类大模型 API。

.agent/skills/add-llm-client/SKILL.md FabioYanezRomero/Knowledge-Graph-Builder

Trigger Scenarios

需要集成新的 LLM 提供商 实现兼容 OpenAI 的本地或云端模型支持

Install

npx skills add FabioYanezRomero/Knowledge-Graph-Builder --skill add-llm-client -g -y
More Options

Non-standard path

npx skills add https://github.com/FabioYanezRomero/Knowledge-Graph-Builder/tree/main/.agent/skills/add-llm-client -g -y

Use without installing

npx skills use FabioYanezRomero/Knowledge-Graph-Builder@add-llm-client

指定 Agent (Claude Code)

npx skills add FabioYanezRomero/Knowledge-Graph-Builder --skill add-llm-client -a claude-code -g -y

安装 repo 全部 skill

npx skills add FabioYanezRomero/Knowledge-Graph-Builder --all -g -y

预览 repo 内 skill

npx skills add FabioYanezRomero/Knowledge-Graph-Builder --list

SKILL.md

Frontmatter
{
    "name": "add-llm-client",
    "description": "Adds a new LLM client provider to the clients module. Use when implementing support for a new LLM provider like Anthropic, OpenAI, Groq, or any OpenAI-compatible API."
}

Adding an LLM Client

This skill documents how to add a new LLM client provider to kgb/clients/.

Overview

LLM clients provide the interface between the extraction pipeline and language model APIs. The system provides:

  • Factory pattern with @client() decorator for auto-registration
  • Abstract base class (BaseLLMClient) with required methods
  • Configuration via ClientConfig dataclass
  • Provider defaults via JSON config files

Architecture

                          Clients Module
    ┌───────────────────────────────────────────────────────────┐
    │                                                           │
    │  base.py              config.py          factory.py       │
    │  ├─ BaseLLMClient     ├─ ClientConfig    ├─ ClientFactory │
    │  └─ LLMClientError    └─ ClientType      └─ @client()    │
    │                                                           │
    │  defaults.py                                              │
    │  └─ load_provider_defaults()  ← reads configs/*.json      │
    │                                                           │
    │  providers/                                               │
    │  ├─ gemini.py         ← Google Gemini (API-based)         │
    │  ├─ ollama.py         ← Ollama (local, OpenAI-compatible) │
    │  ├─ lmstudio.py       ← LM Studio (local, OpenAI-compat) │
    │  └─ your_provider.py  ← Your new client                  │
    │                                                           │
    │  configs/                                                 │
    │  ├─ gemini.json       ← Provider default values           │
    │  ├─ ollama.json                                           │
    │  ├─ lmstudio.json                                         │
    │  └─ your_provider.json                                    │
    │                                                           │
    └───────────────────────────────────────────────────────────┘

Registration Flow:
  @client("name") on class → ClientFactory.register() → ClientFactory.create(config)

Dependencies

Component Library Purpose
LLM framework langextract>=0.1 Structured extraction with source grounding
OpenAI-compatible openai>=1.0 API client for local servers
HTTP requests>=2.28 Direct API calls

ClientConfig Schema

@dataclass
class ClientConfig:
    client_type: ClientType = "gemini"      # str — no Literal constraint
    model_id: str | None = None             # None = use provider default
    temperature: float = 0.0
    max_workers: int | None = None
    max_char_buffer: int = 8000
    show_progress: bool = True
    extraction_passes: int = 1              # langextract passes
    batch_length: int | None = None         # langextract batch size
    api_key: str | None = None              # For API-based clients
    base_url: str | None = None             # For local server clients
    timeout: int = 120

Decision Tree

Scenario Pattern Reference
OpenAI-compatible API openai SDK + langextract ollama.py, lmstudio.py
Native SDK Provider's SDK + langextract gemini.py
REST API (augment) requests or openai All providers' augment()

Step 1: Understand the Interface

All clients must implement BaseLLMClient (defined in kgb/clients/base.py):

class BaseLLMClient(ABC):

    @abstractmethod
    def extract(
        self,
        text: str,
        prompt_description: str,
        examples: list[Any] | None = None,
        format_type: type | None = None,
        temperature: float | None = None,
        max_tokens: int | None = None,
        **kwargs: Any
    ) -> list[dict[str, Any]]:
        """Extract with source grounding (char positions via langextract)."""

    @abstractmethod
    def augment(
        self,
        text: str,
        prompt_description: str,
        format_type: type,
        temperature: float | None = None,
        max_tokens: int | None = None,
        **kwargs: Any
    ) -> list[dict[str, Any]]:
        """Generate inferred triples for graph augmentation.

        Unlike extract(), this does NOT ground in source text (no char positions).
        Used for high-level inference and bridging over an existing graph.
        """

    @classmethod
    @abstractmethod
    def from_config(cls, config: ClientConfig) -> BaseLLMClient:
        """Factory method to create client from configuration."""

extract() vs augment()

extract() augment()
Purpose Extract triples grounded in source text Generate inferred bridging triples
Source grounding Yes — char_start/char_end positions No — no position tracking
Mechanism Uses langextract's full pipeline Direct LLM call (no langextract)
Inference type InferenceType.EXPLICIT InferenceType.CONTEXTUAL
Called by builder/extraction.py builder/augmentation.py

Step 2: Create Provider Defaults

Create kgb/clients/configs/groq.json:

{
  "model_id": "llama-3.1-70b-versatile",
  "base_url": "https://api.groq.com/openai/v1",
  "max_workers": 10
}

These defaults are loaded by load_provider_defaults("groq") when no explicit value is provided.

Step 3: Implement Your Client

Create kgb/clients/providers/groq.py:

"""Groq cloud inference client."""

from __future__ import annotations

import json
from typing import TYPE_CHECKING, Any

import langextract as lx
from langextract.providers.openai import OpenAILanguageModel

from ..base import BaseLLMClient, LLMClientError
from ..defaults import load_provider_defaults
from ..factory import client

if TYPE_CHECKING:
    from ..config import ClientConfig


@client("groq")
class GroqClient(BaseLLMClient):
    """Client for Groq cloud inference."""

    def __init__(
        self,
        model_id: str = "llama-3.1-70b-versatile",
        api_key: str | None = None,
        base_url: str = "https://api.groq.com/openai/v1",
        max_workers: int = 10,
        max_char_buffer: int = 8000,
        show_progress: bool = True,
        timeout: int = 60,
        batch_length: int | None = None,
    ) -> None:
        import os

        self.model_id = model_id
        self.api_key = api_key or os.getenv("GROQ_API_KEY")
        self.base_url = base_url
        self.max_workers = max_workers
        self.max_char_buffer = max_char_buffer
        self.show_progress = show_progress
        self.timeout = timeout
        self.batch_length = batch_length

        if not self.api_key:
            raise LLMClientError("Groq API key required (set GROQ_API_KEY)")

    def extract(
        self,
        text: str,
        prompt_description: str,
        examples: list[Any] | None = None,
        format_type: type | None = None,
        temperature: float | None = None,
        max_tokens: int | None = None,
        **kwargs: Any
    ) -> list[dict[str, Any]]:
        """Extract with source grounding using langextract."""
        try:
            groq_model = OpenAILanguageModel(
                model_id=self.model_id,
                api_key=self.api_key,
                base_url=self.base_url,
                timeout=self.timeout
            )

            result = lx.extract(
                text_or_documents=text,
                prompt_description=prompt_description,
                examples=examples or [],
                model=groq_model,
                temperature=temperature or 0.0,
                max_workers=self.max_workers,
                max_char_buffer=self.max_char_buffer,
                show_progress=self.show_progress,
            )

            items = []
            if hasattr(result, 'extractions'):
                for extraction in result.extractions:
                    if extraction.attributes:
                        item = dict(extraction.attributes)
                        if extraction.char_interval:
                            item["char_start"] = extraction.char_interval.start_pos
                            item["char_end"] = extraction.char_interval.end_pos
                        items.append(item)

            return items

        except Exception as e:
            raise LLMClientError(f"Groq extraction failed: {e}") from e

    def augment(
        self,
        text: str,
        prompt_description: str,
        format_type: type,
        temperature: float | None = None,
        max_tokens: int | None = None,
        **kwargs: Any
    ) -> list[dict[str, Any]]:
        """Generate inferred triples without source grounding."""
        try:
            from openai import OpenAI

            oai = OpenAI(
                api_key=self.api_key,
                base_url=self.base_url,
                timeout=self.timeout
            )

            response = oai.chat.completions.create(
                model=self.model_id,
                messages=[
                    {"role": "system", "content": prompt_description},
                    {"role": "user", "content": text}
                ],
                temperature=temperature or 0.0,
                response_format={"type": "json_object"}
            )

            raw = response.choices[0].message.content
            data = json.loads(raw)

            # Handle both list and nested object responses
            if isinstance(data, list):
                return data
            # Search for a list value in the response
            for value in data.values():
                if isinstance(value, list):
                    return value
            return [data]

        except Exception as e:
            raise LLMClientError(f"Groq augmentation failed: {e}") from e

    @classmethod
    def from_config(cls, config: "ClientConfig") -> "GroqClient":
        defaults = load_provider_defaults("groq")
        return cls(
            model_id=config.model_id or defaults.get("model_id", "llama-3.1-70b-versatile"),
            api_key=config.api_key,
            base_url=config.base_url or defaults.get("base_url", "https://api.groq.com/openai/v1"),
            max_workers=config.max_workers or defaults.get("max_workers", 10),
            max_char_buffer=config.max_char_buffer,
            show_progress=config.show_progress,
            timeout=config.timeout,
            batch_length=config.batch_length or defaults.get("batch_length"),
        )

Step 4: Register Client

Import your provider in kgb/clients/providers/__init__.py:

from .gemini import GeminiClient
from .ollama import OllamaClient
from .lmstudio import LMStudioClient
from .groq import GroqClient  # Add this

__all__ = [
    "GeminiClient",
    "OllamaClient",
    "LMStudioClient",
    "GroqClient",  # Add this
]

The @client("groq") decorator on the class handles factory registration automatically when the module is imported.

Step 5: Verify

Check Registration

python -c "from kgb.clients import ClientFactory; print(ClientFactory.get_available_clients())"
# Output: ['gemini', 'ollama', 'lmstudio', 'groq']

Unit Tests

import pytest
from unittest.mock import Mock, patch, MagicMock
from kgb.clients.providers.groq import GroqClient
from kgb.clients.base import LLMClientError


def test_client_from_config():
    from kgb.clients import ClientConfig

    config = ClientConfig(
        client_type="groq",
        model_id="mixtral-8x7b",
        api_key="test-key"
    )
    client = GroqClient.from_config(config)

    assert client.model_id == "mixtral-8x7b"
    assert client.api_key == "test-key"


def test_missing_api_key():
    with pytest.raises(LLMClientError, match="API key required"):
        GroqClient(api_key=None)


def test_factory_creates_groq():
    from kgb.clients import ClientFactory, ClientConfig

    config = ClientConfig(client_type="groq", api_key="test-key")
    client = ClientFactory.create(config)
    assert isinstance(client, GroqClient)


@patch('kgb.clients.providers.groq.OpenAI')
def test_augment_success(mock_openai):
    mock_client = MagicMock()
    mock_openai.return_value = mock_client
    mock_response = Mock(choices=[Mock(message=Mock(content='[{"head": "A", "relation": "r", "tail": "B"}]'))])
    mock_client.chat.completions.create.return_value = mock_response

    client = GroqClient(api_key="test")
    result = client.augment("text", "desc", dict)

    assert result == [{"head": "A", "relation": "r", "tail": "B"}]


@patch('kgb.clients.providers.groq.OpenAI')
def test_augment_error(mock_openai):
    mock_openai.return_value.chat.completions.create.side_effect = Exception("API error")

    client = GroqClient(api_key="test")
    with pytest.raises(LLMClientError, match="failed"):
        client.augment("text", "desc", dict)

Input/Output Examples

extract() — with char positions (source grounded)

client.extract(
    text="PharmaCorp developed X-123.",
    prompt_description="Extract relationships"
)
# Output:
[{"head": "PharmaCorp", "relation": "developed", "tail": "X-123",
  "char_start": 0, "char_end": 26}]

augment() — without char positions (inferred)

client.augment(
    text="<augmentation prompt with components>",
    prompt_description="Generate bridging triples to connect disconnected components",
    format_type=Triple
)
# Output:
[{"head": "Alice", "relation": "connected_to", "tail": "Acme",
  "inference": "contextual", "justification": "..."}]

CLI Usage

kgb extract --input data.jsonl --domain legal --client groq
kgb extract --input data.jsonl --client groq --model mixtral-8x7b
kgb augment connectivity --input data.jsonl --domain legal --client groq

Key Principles

Principle Implementation
Exception Wrapping raise LLMClientError(...) from e
Lazy Dependencies Import SDKs inside methods
Provider Defaults JSON file in configs/ + load_provider_defaults()
Registration @client("name") decorator auto-registers with factory

Files to Create/Modify

File Action
kgb/clients/providers/groq.py Create — client implementation
kgb/clients/configs/groq.json Create — provider defaults
kgb/clients/providers/__init__.py Modify — add import

Verification Checklist

  • Inherits from BaseLLMClient
  • Implements extract(), augment(), from_config()
  • Decorated with @client("name")
  • Provider defaults JSON in configs/
  • from_config() uses load_provider_defaults()
  • All errors wrapped in LLMClientError
  • Imported in kgb/clients/providers/__init__.py
  • augment() handles varied JSON response structures
  • Tests cover factory creation, defaults, errors

Version History

  • 588f0d9 Current 2026-07-25 05:34

Same Skill Collection

.agent/skills/add-augmentation-strategy/SKILL.md
.agent/skills/add-converter/SKILL.md
.agent/skills/add-dataset-format/SKILL.md
.agent/skills/add-domain/SKILL.md
.agent/skills/add-visualization/SKILL.md

Metadata

Files
0
Version
588f0d9
Hash
1ad805a4
Indexed
2026-07-25 05:34

Accueil - Wiki
Copyright © 2011-2026 iteam. Current version is 2.155.2. UTC+08:00, 2026-08-29 04:08
浙ICP备14020137号-1 $Carte des visiteurs$