Skip to content

Add Langfuse observability integration support#187

Open
hijera wants to merge 13 commits intomainfrom
langfuse-updated
Open

Add Langfuse observability integration support#187
hijera wants to merge 13 commits intomainfrom
langfuse-updated

Conversation

@hijera
Copy link
Copy Markdown
Collaborator

@hijera hijera commented Mar 26, 2026

Changes

  • Configuration: Added langfuse flag to AgentConfig for enabling Langfuse tracing

    • Configurable via YAML (langfuse: true/false) or environment variable (SGR__LANGFUSE)
    • Defaults to false
  • Agent Factory: Updated _create_client() to support Langfuse AsyncOpenAI client

    • Uses langfuse.openai.AsyncOpenAI when enabled
    • Falls back gracefully to standard openai.AsyncOpenAI if Langfuse package unavailable
    • Added _patch_langfuse_stream_close() to fix streaming compatibility issues
  • Dependencies: Added langfuse>=4.0.0 to project dependencies

  • Documentation: Added Langfuse configuration guide in English and Russian

    • Explains how to enable/configure Langfuse
    • Documents required environment variables
  • Tests: Added comprehensive test coverage

    • Configuration loading from YAML and environment
    • Langfuse client creation and fallback behavior
    • Integration tests for configuration consistency

Comment thread sgr_agent_core/agent_definition.py Outdated
Comment on lines +179 to +181
@property
def langfuse_enabled(self) -> bool:
return self.langfuse.enabled
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Выглядит излишним

Comment thread sgr_agent_core/agent_factory.py Outdated
Comment on lines +84 to +107
if getattr(config, "langfuse_enabled", False):
try:
lf_cfg = config.langfuse
if lf_cfg.public_key or lf_cfg.secret_key or lf_cfg.host:
LangfuseClient = getattr(import_module("langfuse"), "Langfuse")
kwargs = {}
if lf_cfg.public_key:
kwargs["public_key"] = lf_cfg.public_key
if lf_cfg.secret_key:
kwargs["secret_key"] = lf_cfg.secret_key
if lf_cfg.host:
kwargs["host"] = lf_cfg.host
LangfuseClient(**kwargs)
logger.info("Langfuse initialized with explicit credentials from config")
LangfuseAsyncOpenAI = getattr(import_module("langfuse.openai"), "AsyncOpenAI")
cls._patch_langfuse_stream_close()
logger.info("Creating Langfuse AsyncOpenAI client (langfuse_enabled=True)")
return LangfuseAsyncOpenAI(**client_kwargs)
except ImportError:
logger.warning(
"Langfuse is enabled but 'langfuse' package is not available. "
"Falling back to standard AsyncOpenAI client."
)

Copy link
Copy Markdown
Member

@virrius virrius Mar 30, 2026

Choose a reason for hiding this comment

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

Что-то не очень здоровое.

  1. Валидацию можно вынести в модельку
  2. Зачем-то собираются лишние kwargs с лишними if
  3. Если юзер не заимпортил модуль, лучше явно упасть чем неявно фолбекнуться
  4. Какую проблему решает патч stream close?

прогонял на этом тесте, вроде всё работает

    from sgr_agent_core.agent_config import GlobalConfig
    config = GlobalConfig().from_yaml("config.yaml")
    from langfuse import Langfuse
    Langfuse(
        public_key=config.langfuse.public_key,
        secret_key=config.langfuse.secret_key,
        host=config.langfuse.host,
    )
    from langfuse.openai import AsyncOpenAI
    async_client = AsyncOpenAI(
        base_url=config.llm.base_url,
        api_key=config.llm.api_key,
    )
    completion = await async_client.chat.completions.create(
      name="test-chat",
      model="gpt-4o",
      messages=[
          {"role": "system", "content": "Tell me about dirigables"},
          {"role": "user", "content": "Tell me about dirigables"}],
      temperature=0,
      metadata={"someMetadataKey": "someValue"},
      stream=True
    )
    async for chunk in completion:
        print(chunk.choices[0].delta.content, end="")
    print(Langfuse)
    print(completion)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

  1. через kwargs собирается, потому что Langfuse с пустыми параметрами ( когда kwargs будут пустые) инициализируется с env переменными LANGFUSE_SECRET_KEY ,
    LANGFUSE_PUBLIC_KEY ,
    LANGFUSE_BASE_URL .

Copy link
Copy Markdown
Member

@virrius virrius Mar 30, 2026

Choose a reason for hiding this comment

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

  1. У нас уже есть готовая pydantic моделька, которая в себе это хранит, валидирует и всё такое. Зачем возвращаться обратно к raw словарю?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

  1. ЛЛМки говорят, что
    Кратко: патч нужен не для того, чтобы итерация async for chunk in completion вообще заработала, а для того, чтобы корректно закрыть стрим, когда внутренний код OpenAI вызывает aclose() на обёрнутом ответе Langfuse.

Что именно ломалось
В agent_factory.py в docstring патча сказано: у обёртки Langfuse на HTTP-ответе иногда есть только синхронный close(), а класс OpenAI AsyncChatCompletionStream в методе close ожидает aclose() на _response. Тогда при закрытии стрима возникает AttributeError (в доке — про отсутствие aclose).

То есть проблема — на этапе закрытия / cleanup, а не на чтении чанков.
Почему «всё работает»
Версии пакетов — в pyproject.toml у вас langfuse>=4.0.0 и современный openai. В новых релизах Langfuse или OpenAI обёртка/стриминг могли измениться так, что либо у ответа появился aclose(), либо путь закрытия другой — и баг просто не проявляется на вашей связке.

Сценарий «дочитали до конца и вышли» — основной поток async for успевает отдать все чанки; ошибка могла бы всплыть после цикла, при закрытии контекста/деструкторе/сборке мусора — не всегда это видно как явный traceback в простом скрипте.

Другой кодовый путь — ваш ручной скрипт и путь через AgentFactory._create_client + тот же AsyncOpenAI теоретически могут отличаться окружением (версии, флаги), но чаще всего решает именно п.1–2.

Copy link
Copy Markdown
Member

@virrius virrius Apr 1, 2026

Choose a reason for hiding this comment

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

Так, и что из этого должно следовать?

Copy link
Copy Markdown
Collaborator Author

@hijera hijera Apr 2, 2026

Choose a reason for hiding this comment

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

что возможно когда-то оно может сломаться, но я не уверен =/ Ок. могу убрать

@hijera hijera mentioned this pull request Apr 20, 2026
@EvilFreelancer EvilFreelancer self-requested a review April 21, 2026 13:44
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.

3 participants