Skip to content

feat(skill): skills management client, cache and manager methods#1739

Open
Robin1511 wants to merge 2 commits into
langfuse:mainfrom
Robin1511:feat/skills-management
Open

feat(skill): skills management client, cache and manager methods#1739
Robin1511 wants to merge 2 commits into
langfuse:mainfrom
Robin1511:feat/skills-management

Conversation

@Robin1511

@Robin1511 Robin1511 commented Jul 7, 2026

Copy link
Copy Markdown

Skills management (Python SDK)

Hand-written high-level skills API mirroring the prompt manager:

  • SkillCache (TTL, background stale-while-revalidate refresh, prefix invalidation)
  • SkillClient with .compile() variable substitution of the instructions body (single client — skills have no chat/text split)
  • Langfuse methods get_skill / create_skill / update_skill / delete_skill / clear_skill_cache with caching, fallback, retries
  • unit tests for cache TTL and client compilation

Depends on the auto-generated langfuse.api skills client from the api-spec-bot regeneration of the Fern contract in langfuse/langfuse (langfuse/langfuse#14838). Imports resolve once that client lands.

Greptile Summary

This PR adds a skills management API to the Python SDK — mirroring the existing prompt manager — with SkillCache (TTL + stale-while-revalidate background refresh), SkillClient (variable substitution via compile()), and five new Langfuse methods (get_skill, create_skill, update_skill, delete_skill, clear_skill_cache).

  • SkillCache and SkillCacheTaskManager are near-exact copies of the PromptCache infrastructure; the background-refresh deduplication and shutdown logic work correctly.
  • SkillClient wraps the generated Skill API model with compile() delegating to TemplateParser and a dict() helper for serialization.
  • Two mutable default arguments (labels=[] in create_skill, new_labels=[] in update_skill) and a prefix-matching bug in SkillCache.invalidate need attention before merge.

Confidence Score: 3/5

Needs three straightforward fixes before merge: two mutable default arguments and a cache invalidation prefix bug.

The invalidate method does a bare startswith(skill_name) check, which silently evicts cache entries for any skill whose name starts with the target name. The two mutable [] defaults in create_skill/update_skill are a latent correctness hazard. All three are straightforward one-line fixes, but they affect the correctness of the core cache and public API surface.

langfuse/_utils/skill_cache.py (invalidate prefix logic) and langfuse/_client/client.py (mutable defaults, docstring unit error).

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant Caller
    participant Langfuse
    participant SkillCache
    participant API as Langfuse API

    Caller->>Langfuse: get_skill(name, version, label, cache_ttl_seconds)
    Langfuse->>SkillCache: get(cache_key)
    alt "Cache miss or ttl==0"
        SkillCache-->>Langfuse: None
        Langfuse->>API: skills.get(name, version, label)
        API-->>Langfuse: Skill response
        Langfuse->>SkillCache: set(cache_key, SkillClient, ttl)
        Langfuse-->>Caller: SkillClient
    else Cache hit fresh
        SkillCache-->>Langfuse: SkillCacheItem not expired
        Langfuse-->>Caller: cached_skill.value
    else Cache hit stale expired
        SkillCache-->>Langfuse: SkillCacheItem expired
        Langfuse->>SkillCache: add_refresh_skill_task_if_current
        Note over SkillCache: Background thread refreshes cache
        Langfuse-->>Caller: stale cached_skill.value
    end

    Caller->>Langfuse: create_skill(name, instructions, labels)
    Langfuse->>API: skills.create(CreateSkillRequest)
    API-->>Langfuse: Skill
    Langfuse->>SkillCache: invalidate(name)
    Langfuse-->>Caller: SkillClient
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant Caller
    participant Langfuse
    participant SkillCache
    participant API as Langfuse API

    Caller->>Langfuse: get_skill(name, version, label, cache_ttl_seconds)
    Langfuse->>SkillCache: get(cache_key)
    alt "Cache miss or ttl==0"
        SkillCache-->>Langfuse: None
        Langfuse->>API: skills.get(name, version, label)
        API-->>Langfuse: Skill response
        Langfuse->>SkillCache: set(cache_key, SkillClient, ttl)
        Langfuse-->>Caller: SkillClient
    else Cache hit fresh
        SkillCache-->>Langfuse: SkillCacheItem not expired
        Langfuse-->>Caller: cached_skill.value
    else Cache hit stale expired
        SkillCache-->>Langfuse: SkillCacheItem expired
        Langfuse->>SkillCache: add_refresh_skill_task_if_current
        Note over SkillCache: Background thread refreshes cache
        Langfuse-->>Caller: stale cached_skill.value
    end

    Caller->>Langfuse: create_skill(name, instructions, labels)
    Langfuse->>API: skills.create(CreateSkillRequest)
    API-->>Langfuse: Skill
    Langfuse->>SkillCache: invalidate(name)
    Langfuse-->>Caller: SkillClient
Loading
Prompt To Fix All With AI
Fix the following 4 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 4
langfuse/_client/client.py:4241-4252
Mutable default argument for `labels` — the same list object is shared across all calls where `labels` is not provided. If any code path (e.g., a Pydantic validator or future caller) appends to this list, every subsequent call that uses the default will see the mutated value. The idiomatic Python fix is to default to `None` and substitute an empty list in the function body. The same applies to `new_labels` in `update_skill`.

```suggestion
    def create_skill(
        self,
        *,
        name: str,
        instructions: str,
        description: Optional[str] = None,
        labels: Optional[List[str]] = None,
        tags: Optional[List[str]] = None,
        metadata: Optional[Any] = None,
        allowed_tools: Optional[List[str]] = None,
        commit_message: Optional[str] = None,
    ) -> SkillClient:
```

### Issue 2 of 4
langfuse/_client/client.py:4293-4299
Mutable default argument for `new_labels` — same issue as in `create_skill`. Default to `None` and coerce to `[]` inside the function body before passing to the API call.

```suggestion
    def update_skill(
        self,
        *,
        name: str,
        version: int,
        new_labels: Optional[List[str]] = None,
    ) -> Any:
```

### Issue 3 of 4
langfuse/_utils/skill_cache.py:179-184
Prefix matching without a separator anchor can silently invalidate cache entries for a different skill. If a skill named `"foo"` exists alongside a skill named `"foo-bar"`, calling `invalidate("foo")` also deletes `"foo-bar-version:1"` because `"foo-bar-version:1".startswith("foo")` is `True`. Since `generate_cache_key` always appends a `-` separator after the name, anchoring the check to `skill_name + "-"` is sufficient to prevent cross-skill eviction. The same pattern exists in `PromptCache.invalidate`.

```suggestion
    def invalidate(self, skill_name: str) -> None:
        """Invalidate all cached skills with the given skill name."""
        prefix = skill_name + "-"
        with self._lock:
            for key in list(self._cache):
                if key.startswith(prefix):
                    del self._cache[key]
```

### Issue 4 of 4
langfuse/_client/client.py:4091
The docstring says the timeout is "in milliseconds" but the parameter is named `fetch_timeout_seconds` and is passed directly to the `timeout_in_seconds` request option, so the unit is seconds — not milliseconds.

```suggestion
            fetch_timeout_seconds: Optional[int]: The timeout in seconds for fetching the skill. Defaults to the default timeout set on the SDK, which is 5 seconds per default.
```

Reviews (1): Last reviewed commit: "feat(skill): add skills management clien..." | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

Hand-written high-level skills API mirroring the prompt manager:

- SkillCache (TTL, background stale-while-revalidate refresh, prefix
  invalidation) in langfuse/_utils/skill_cache.py
- SkillClient wrapper with .compile() variable substitution of the
  instructions body (single client; skills have no chat/text split)
- Langfuse client methods get_skill / create_skill / update_skill /
  delete_skill / clear_skill_cache with caching, fallback and retries
- unit tests for cache TTL and client compilation

Depends on the auto-generated langfuse.api skills client (api-spec-bot
regeneration from the Fern contract in langfuse/langfuse).

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This pull request is from a fork — automated review is disabled. A repository maintainer can comment @claude review to run a one-time review.

@CLAassistant

CLAassistant commented Jul 7, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

Comment thread langfuse/_client/client.py
Comment thread langfuse/_client/client.py
Comment thread langfuse/_utils/skill_cache.py
…ocstring

- create_skill/update_skill: default labels/new_labels to None and coerce
  to [] in the body (avoid shared mutable default list)
- SkillCache.invalidate: anchor prefix match with '-' separator so a skill
  named 'foo' cannot evict 'foobar-*' cache entries
- get_skill: docstring fetch_timeout_seconds is seconds, not milliseconds
@Robin1511 Robin1511 force-pushed the feat/skills-management branch from ca4659b to 1952f27 Compare July 7, 2026 13:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants