Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions docs/adapters/frameworks-semantic_kernel.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# Semantic Kernel framework adapter

`layerlens.instrument.adapters.frameworks.semantic_kernel.SemanticKernelAdapter`
instruments [Microsoft Semantic Kernel](https://github.com/microsoft/semantic-kernel)
using the kernel's native filter API — non-invasive, no monkey-patching.

## Install

```bash
pip install 'layerlens[semantic-kernel]'
```

Pulls `semantic-kernel>=1.0,<2.0`. Requires Python 3.10+.

## Quick start

```python
import asyncio
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion

from layerlens.instrument.adapters.frameworks.semantic_kernel import SemanticKernelAdapter
from layerlens.instrument.transport.sink_http import HttpEventSink

sink = HttpEventSink(adapter_name="semantic_kernel")
adapter = SemanticKernelAdapter()
adapter.add_sink(sink)
adapter.connect()

kernel = Kernel()
kernel.add_service(OpenAIChatCompletion(ai_model_id="gpt-4o-mini"))
adapter.instrument_kernel(kernel)

async def main() -> None:
result = await kernel.invoke_prompt("What is 2 + 2?")
print(result)

asyncio.run(main())

adapter.disconnect()
sink.close()
```

## What's wrapped

`adapter.instrument_kernel(kernel)` registers three Semantic Kernel filters
on the supplied kernel:

- `function_invocation_filter` — fires before/after every `KernelFunction`
call (plugin function, prompt function, etc.).
- `prompt_rendering_filter` — fires before/after the prompt template is
rendered for prompt functions.
- `auto_function_invocation_filter` — fires when the model auto-selects a
plugin function via tool-calling.

No methods are monkey-patched; on `disconnect()` the filter list is cleared
and the kernel returns to its original behaviour.

## Events emitted

| Event | Layer | When |
|---|---|---|
| `environment.config` | L4a | First plugin invocation per kernel. |
| `agent.input` | L1 | Function invocation start. |
| `agent.output` | L1 | Function invocation end (success or error). |
| `agent.code` | L2 | Per plugin function when `l2_agent_code` is true. |
| `agent.action` | L4a | Per planner step. |
| `agent.state.change` | cross-cutting | Memory store reads/writes. |
| `tool.call` | L5a | Per `auto_function_invocation` (model-selected plugin). |
| `model.invoke` | L3 | Per LLM call inside the kernel. |

## Semantic Kernel specifics

- **Plugin attribution**: every event includes `plugin_name`,
`function_name`, and (for prompt functions) the rendered prompt token
count when available.
- **Filter API is preferred**: filters are first-class Semantic Kernel
citizens — they survive kernel cloning and don't break the type system.
This is why this adapter uses filters instead of method-wrapping.
- **Async-first**: Semantic Kernel is async-first; all filters are async
and propagate the `next` continuation correctly.

## Capture config

```python
from layerlens.instrument.adapters._base import CaptureConfig

# Recommended.
adapter = SemanticKernelAdapter(capture_config=CaptureConfig.standard())

# Capture rendered prompt template body.
adapter = SemanticKernelAdapter(
capture_config=CaptureConfig(
l1_agent_io=True,
l3_model_metadata=True,
l5a_tool_calls=True,
capture_content=True,
),
)
```

## BYOK

Semantic Kernel uses `OpenAIChatCompletion`, `AzureChatCompletion`,
`HuggingFacePromptExecutionSettings`, etc. for model access. The adapter
does not own those credentials. For platform-managed BYOK see
`docs/adapters/byok.md` (atlas-app M1.B).
14 changes: 13 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ classifiers = [
[project.optional-dependencies]
cli = ["click>=8.0.0"]

# --- Instrument layer: framework adapters ---
# Adding any extra below MUST keep the default `pip install layerlens`
# install set unchanged. Verified by `tests/instrument/test_default_install.py`.
semantic-kernel = ["semantic-kernel>=1.0,<2.0; python_version >= '3.10'"]

[project.urls]
Homepage = "https://github.com/LayerLens/stratix-python"
Repository = "https://github.com/LayerLens/stratix-python"
Expand Down Expand Up @@ -139,14 +144,21 @@ known-first-party = ["openai", "tests"]
"tests/**.py" = ["T201", "T203", "ARG", "B007"]
"examples/**.py" = ["T201", "T203"]
"src/layerlens/cli/**" = ["T201", "T203"]
# Framework callbacks have signatures dictated by upstream — unused
# arguments are part of the contract, not a code smell.
"src/layerlens/instrument/adapters/frameworks/**.py" = ["ARG002"]

[tool.pyright]
include = ["src", "tests"]
exclude = ["**/__pycache__"]
reportMissingTypeStubs = false

# Less strict settings for tests and cli
# Less strict settings for tests, cli, and the dynamic-monkey-patching
# adapter code. mypy --strict stays strict for these dirs; pyright is
# relaxed here because it can't follow runtime attribute mutation that
# the framework instrumentation relies on.
executionEnvironments = [
{ root = "src/layerlens/cli", reportMissingImports = false, reportFunctionMemberAccess = false, reportCallIssue = false, reportArgumentType = false, reportAttributeAccessIssue = false },
{ root = "src/layerlens/instrument/adapters/frameworks", reportPossiblyUnbound = false, reportPossiblyUnboundVariable = false, reportCallIssue = false, reportAttributeAccessIssue = false, reportArgumentType = false, reportMissingImports = false, reportFunctionMemberAccess = false },
{ root = "tests", reportGeneralTypeIssues = false, reportOptionalSubscript = false, reportOptionalMemberAccess = false, reportUntypedFunctionDecorator = false, reportUnknownArgumentType = false, reportUnknownMemberType = false, reportUnknownVariableType = false, reportUnnecessaryIsInstance = false, reportUnnecessaryComparison = false, reportArgumentType = false, reportCallIssue = false },
]
42 changes: 42 additions & 0 deletions samples/instrument/semantic_kernel/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Semantic Kernel sample

Runnable end-to-end sample for the Microsoft Semantic Kernel framework
adapter. The script wires the LayerLens filters via
`SemanticKernelAdapter.instrument_kernel(kernel)` and runs a single
`invoke_prompt` call against an `OpenAIChatCompletion` service.

## Install

```bash
pip install 'layerlens[semantic-kernel,providers-openai]'
```

This pulls `semantic-kernel>=1.0,<2.0` (which itself depends on
`openai>=1.30`). Requires Python 3.10+.

## Run

```bash
export OPENAI_API_KEY=sk-...
export LAYERLENS_STRATIX_API_KEY=... # optional — needed only to ship spans
export LAYERLENS_STRATIX_BASE_URL=... # optional — defaults to LayerLens cloud

python -m samples.instrument.semantic_kernel.main
```

The sample prints the model's response and ships an
`agent.input` / `agent.output` / `model.invoke` event triple to
atlas-app via `HttpEventSink`. If the LayerLens credentials are not set,
the sink buffers the events in memory and drops them on shutdown — the
SK call still runs.

## What this exercises

- `SemanticKernelAdapter` lifecycle (`connect` → `instrument_kernel` →
`disconnect`).
- All three SK filters: `function_invocation`, `prompt_rendering`,
`auto_function_invocation`.
- The HTTP transport sink batched flush path.

For the full adapter reference see
[`docs/adapters/frameworks-semantic_kernel.md`](../../../docs/adapters/frameworks-semantic_kernel.md).
Empty file.
86 changes: 86 additions & 0 deletions samples/instrument/semantic_kernel/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Sample: instrument a Semantic Kernel prompt invocation with LayerLens.

Builds a ``Kernel`` with an OpenAI chat completion service, registers the
LayerLens filters via ``SemanticKernelAdapter.instrument_kernel``, and runs a
single ``invoke_prompt`` call. Filter callbacks emit ``agent.input`` /
``agent.output`` / ``model.invoke`` events that ship to atlas-app via
``HttpEventSink``.

Required environment:

* ``OPENAI_API_KEY`` — used by ``OpenAIChatCompletion``.
* ``LAYERLENS_STRATIX_API_KEY`` — your LayerLens API key (optional).
* ``LAYERLENS_STRATIX_BASE_URL`` — atlas-app base URL (optional).

Run::

pip install 'layerlens[semantic-kernel,providers-openai]'
python -m samples.instrument.semantic_kernel.main
"""

from __future__ import annotations

import os
import sys
import asyncio

from layerlens.instrument.adapters._base import CaptureConfig
from layerlens.instrument.transport.sink_http import HttpEventSink
from layerlens.instrument.adapters.frameworks.semantic_kernel import SemanticKernelAdapter


async def _run(kernel: object) -> str:
# Imported here to keep the top-level module importable without semantic-kernel.
from semantic_kernel.functions import KernelArguments # type: ignore[import-not-found,unused-ignore]

result = await kernel.invoke_prompt( # type: ignore[attr-defined]
prompt="Reply with just the digit. What is 2 + 2?",
arguments=KernelArguments(),
)
return str(result)


def main() -> int:
if not os.environ.get("OPENAI_API_KEY"):
print("OPENAI_API_KEY is not set; cannot run sample.", file=sys.stderr)
return 2

try:
from semantic_kernel import Kernel
from semantic_kernel.connectors.ai.open_ai import OpenAIChatCompletion
except ImportError:
print(
"semantic-kernel not installed. Install with:\n"
" pip install 'layerlens[semantic-kernel,providers-openai]'",
file=sys.stderr,
)
return 2

sink = HttpEventSink(
adapter_name="semantic_kernel",
path="/telemetry/spans",
max_batch=10,
flush_interval_s=1.0,
)

adapter = SemanticKernelAdapter(capture_config=CaptureConfig.standard())
adapter.add_sink(sink)
adapter.connect()

kernel = Kernel()
kernel.add_service(OpenAIChatCompletion(ai_model_id="gpt-4o-mini"))
adapter.instrument_kernel(kernel)

try:
response = asyncio.run(_run(kernel))
print(f"Response: {response}")
finally:
sink.close()
adapter.disconnect()

print("Telemetry shipped. Check the LayerLens dashboard adapter health page.")
return 0


if __name__ == "__main__":
raise SystemExit(main())
17 changes: 17 additions & 0 deletions src/layerlens/instrument/adapters/frameworks/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Framework adapters for the LayerLens Instrument layer.

Each framework adapter wraps an agent / chain framework's lifecycle to
intercept agent runs, model invocations, tool calls, state changes, and
handoffs, emitting events through the LayerLens telemetry pipeline.

Adapter packages exported by the registry (loaded on demand by
:class:`AdapterRegistry`):

* ``semantic_kernel`` — Microsoft Semantic Kernel (filter API).

Importing this package does NOT import any framework SDK. Each
``frameworks.<name>`` package is loaded only when the user requests it
explicitly, e.g. via ``AdapterRegistry.get("semantic_kernel")``.
"""

from __future__ import annotations
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""LayerLens Semantic Kernel Adapter.

Provides plugin invocation tracing, planner execution tracking, and
memory operation capture for Microsoft Semantic Kernel via the kernel's
native filter API.

Importing this module does NOT import ``semantic-kernel`` itself — that
dependency is only required when the user calls
:meth:`SemanticKernelAdapter.instrument_kernel` against a real kernel.
"""

from __future__ import annotations

from layerlens.instrument.adapters.frameworks.semantic_kernel.filters import (
LayerLensFunctionFilter,
LayerLensAutoFunctionFilter,
LayerLensPromptRenderFilter,
)
from layerlens.instrument.adapters.frameworks.semantic_kernel.metadata import (
SKMetadataExtractor,
)
from layerlens.instrument.adapters.frameworks.semantic_kernel.lifecycle import (
StratixMemoryStore,
SemanticKernelAdapter,
)

# Registry lazy-loading convention.
ADAPTER_CLASS = SemanticKernelAdapter

# Backward-compat aliases for users coming from ateam (``STRATIX*`` →
# ``LayerLens*``). The class objects are identical; only the import name
# changes. Slated for removal in the next major SDK release.
STRATIXFunctionFilter = LayerLensFunctionFilter # noqa: N816 - backward-compat alias
STRATIXAutoFunctionFilter = LayerLensAutoFunctionFilter # noqa: N816 - backward-compat alias
STRATIXPromptRenderFilter = LayerLensPromptRenderFilter # noqa: N816 - backward-compat alias

__all__ = [
"ADAPTER_CLASS",
"LayerLensAutoFunctionFilter",
"LayerLensFunctionFilter",
"LayerLensPromptRenderFilter",
"SKMetadataExtractor",
"STRATIXAutoFunctionFilter",
"STRATIXFunctionFilter",
"STRATIXPromptRenderFilter",
"SemanticKernelAdapter",
"StratixMemoryStore",
]
Loading