refactor: move tracers to respective files (#2734)
* refactor: move Tracers to respective files * feat(pyproject.toml): update langwatch version to 0.1.9 and add langsmith dependency at version 0.1.86
This commit is contained in:
parent
5346db0d0c
commit
50dc1e87d1
7 changed files with 341 additions and 2748 deletions
10
poetry.lock
generated
10
poetry.lock
generated
|
|
@ -4941,7 +4941,6 @@ asyncer = "^0.0.5"
|
||||||
bcrypt = "4.0.1"
|
bcrypt = "4.0.1"
|
||||||
cachetools = "^5.3.1"
|
cachetools = "^5.3.1"
|
||||||
chardet = "^5.2.0"
|
chardet = "^5.2.0"
|
||||||
crewai = "^0.36.0"
|
|
||||||
cryptography = "^42.0.5"
|
cryptography = "^42.0.5"
|
||||||
docstring-parser = "^0.16"
|
docstring-parser = "^0.16"
|
||||||
duckdb = "^1.0.0"
|
duckdb = "^1.0.0"
|
||||||
|
|
@ -4954,7 +4953,6 @@ jq = {version = "^1.7.0", markers = "sys_platform != \"win32\""}
|
||||||
langchain = "~0.2.0"
|
langchain = "~0.2.0"
|
||||||
langchain-experimental = "^0.0.61"
|
langchain-experimental = "^0.0.61"
|
||||||
langchainhub = "~0.1.15"
|
langchainhub = "~0.1.15"
|
||||||
langwatch = "^0.1.3"
|
|
||||||
loguru = "^0.7.1"
|
loguru = "^0.7.1"
|
||||||
multiprocess = "^0.70.14"
|
multiprocess = "^0.70.14"
|
||||||
nest-asyncio = "^1.6.0"
|
nest-asyncio = "^1.6.0"
|
||||||
|
|
@ -5038,13 +5036,13 @@ requests = ">=2,<3"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "langwatch"
|
name = "langwatch"
|
||||||
version = "0.1.8"
|
version = "0.1.9"
|
||||||
description = "Python SDK for LangWatch for monitoring your LLMs"
|
description = "Python SDK for LangWatch for monitoring your LLMs"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "<4.0,>=3.9"
|
python-versions = "<4.0,>=3.9"
|
||||||
files = [
|
files = [
|
||||||
{file = "langwatch-0.1.8-py3-none-any.whl", hash = "sha256:e6e7ffe2f3cd61c477e8ff6de4ad3d1e06f63b0c8f02880d68293fc126ec2bf7"},
|
{file = "langwatch-0.1.9-py3-none-any.whl", hash = "sha256:d082f70bb7f6aa67623281e3e624065932bc6e5729322637d8957cc63942a007"},
|
||||||
{file = "langwatch-0.1.8.tar.gz", hash = "sha256:2ccc3a741ef9bf493946264ab8fff5cb33845e51d4426136218d62b1a4cbd26d"},
|
{file = "langwatch-0.1.9.tar.gz", hash = "sha256:3f044b668239b0e4cc244635f8ceeb7e10948c736f6b60bcda7c26852308b2f8"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -11737,4 +11735,4 @@ local = ["ctransformers", "llama-cpp-python", "sentence-transformers"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = ">=3.10,<3.13"
|
python-versions = ">=3.10,<3.13"
|
||||||
content-hash = "afe38792a9e202ab55558389026b64beb95d1437b2fd4bccfbb300cbc7ad0677"
|
content-hash = "53efe77ea0fe985d9319884d4e0ade4d1949f6348ecf75d6afa7297483dd314c"
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,8 @@ langchain-nvidia-ai-endpoints = "^0.1.2"
|
||||||
langchain-google-calendar-tools = "^0.0.1"
|
langchain-google-calendar-tools = "^0.0.1"
|
||||||
langchain-milvus = "^0.1.1"
|
langchain-milvus = "^0.1.1"
|
||||||
crewai = {extras = ["tools"], version = "^0.36.0"}
|
crewai = {extras = ["tools"], version = "^0.36.0"}
|
||||||
langwatch = "^0.1.7"
|
langwatch = "^0.1.9"
|
||||||
|
langsmith = "^0.1.86"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
|
|
||||||
151
src/backend/base/langflow/services/tracing/langsmith.py
Normal file
151
src/backend/base/langflow/services/tracing/langsmith.py
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
import os
|
||||||
|
import traceback
|
||||||
|
import types
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, Optional
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from langflow.schema.data import Data
|
||||||
|
from langflow.services.tracing.base import BaseTracer
|
||||||
|
from langflow.services.tracing.schema import Log
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from langflow.graph.vertex.base import Vertex
|
||||||
|
|
||||||
|
|
||||||
|
class LangSmithTracer(BaseTracer):
|
||||||
|
def __init__(self, trace_name: str, trace_type: str, project_name: str, trace_id: UUID):
|
||||||
|
from langsmith.run_trees import RunTree
|
||||||
|
|
||||||
|
self.trace_name = trace_name
|
||||||
|
self.trace_type = trace_type
|
||||||
|
self.project_name = project_name
|
||||||
|
self.trace_id = trace_id
|
||||||
|
try:
|
||||||
|
self._run_tree = RunTree(
|
||||||
|
project_name=self.project_name,
|
||||||
|
name=self.trace_name,
|
||||||
|
run_type=self.trace_type,
|
||||||
|
id=self.trace_id,
|
||||||
|
)
|
||||||
|
self._run_tree.add_event({"name": "Start", "time": datetime.now(timezone.utc).isoformat()})
|
||||||
|
self._children: dict[str, RunTree] = {}
|
||||||
|
self._ready = self.setup_langsmith()
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error setting up LangSmith tracer: {e}")
|
||||||
|
self._ready = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ready(self):
|
||||||
|
return self._ready
|
||||||
|
|
||||||
|
def setup_langsmith(self):
|
||||||
|
try:
|
||||||
|
from langsmith import Client
|
||||||
|
|
||||||
|
self._client = Client()
|
||||||
|
except ImportError:
|
||||||
|
logger.error("Could not import langsmith. Please install it with `pip install langsmith`.")
|
||||||
|
return False
|
||||||
|
os.environ["LANGCHAIN_TRACING_V2"] = "true"
|
||||||
|
return True
|
||||||
|
|
||||||
|
def add_trace(
|
||||||
|
self,
|
||||||
|
trace_id: str,
|
||||||
|
trace_name: str,
|
||||||
|
trace_type: str,
|
||||||
|
inputs: Dict[str, Any],
|
||||||
|
metadata: Dict[str, Any] | None = None,
|
||||||
|
vertex: Optional["Vertex"] = None,
|
||||||
|
):
|
||||||
|
if not self._ready:
|
||||||
|
return
|
||||||
|
processed_inputs = {}
|
||||||
|
if inputs:
|
||||||
|
processed_inputs = self._convert_to_langchain_types(inputs)
|
||||||
|
child = self._run_tree.create_child(
|
||||||
|
name=trace_name,
|
||||||
|
run_type=trace_type, # type: ignore[arg-type]
|
||||||
|
inputs=processed_inputs,
|
||||||
|
)
|
||||||
|
if metadata:
|
||||||
|
child.add_metadata(self._convert_to_langchain_types(metadata))
|
||||||
|
self._children[trace_name] = child
|
||||||
|
self._child_link: dict[str, str] = {}
|
||||||
|
|
||||||
|
def _convert_to_langchain_types(self, io_dict: Dict[str, Any]):
|
||||||
|
converted = {}
|
||||||
|
for key, value in io_dict.items():
|
||||||
|
converted[key] = self._convert_to_langchain_type(value)
|
||||||
|
return converted
|
||||||
|
|
||||||
|
def _convert_to_langchain_type(self, value):
|
||||||
|
from langflow.schema.message import Message
|
||||||
|
|
||||||
|
if isinstance(value, dict):
|
||||||
|
for key, _value in value.copy().items():
|
||||||
|
_value = self._convert_to_langchain_type(_value)
|
||||||
|
value[key] = _value
|
||||||
|
elif isinstance(value, list):
|
||||||
|
value = [self._convert_to_langchain_type(v) for v in value]
|
||||||
|
elif isinstance(value, Message):
|
||||||
|
if "prompt" in value:
|
||||||
|
value = value.load_lc_prompt()
|
||||||
|
elif value.sender:
|
||||||
|
value = value.to_lc_message()
|
||||||
|
else:
|
||||||
|
value = value.to_lc_document()
|
||||||
|
elif isinstance(value, Data):
|
||||||
|
value = value.to_lc_document()
|
||||||
|
elif isinstance(value, types.GeneratorType):
|
||||||
|
# generator is not serializable, also we can't consume it
|
||||||
|
value = str(value)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def end_trace(
|
||||||
|
self,
|
||||||
|
trace_id: str,
|
||||||
|
trace_name: str,
|
||||||
|
outputs: Dict[str, Any] | None = None,
|
||||||
|
error: Exception | None = None,
|
||||||
|
logs: list[Log | dict] = [],
|
||||||
|
):
|
||||||
|
child = self._children[trace_name]
|
||||||
|
raw_outputs = {}
|
||||||
|
processed_outputs = {}
|
||||||
|
if outputs:
|
||||||
|
raw_outputs = outputs
|
||||||
|
processed_outputs = self._convert_to_langchain_types(outputs)
|
||||||
|
if logs:
|
||||||
|
child.add_metadata(self._convert_to_langchain_types({"logs": {log.get("name"): log for log in logs}}))
|
||||||
|
child.add_metadata(self._convert_to_langchain_types({"outputs": raw_outputs}))
|
||||||
|
child.end(outputs=processed_outputs, error=self._error_to_string(error))
|
||||||
|
if error:
|
||||||
|
child.patch()
|
||||||
|
else:
|
||||||
|
child.post()
|
||||||
|
self._child_link[trace_name] = child.get_url()
|
||||||
|
|
||||||
|
def _error_to_string(self, error: Optional[Exception]):
|
||||||
|
error_message = None
|
||||||
|
if error:
|
||||||
|
string_stacktrace = traceback.format_exception(error)
|
||||||
|
error_message = f"{error.__class__.__name__}: {error}\n\n{string_stacktrace}"
|
||||||
|
return error_message
|
||||||
|
|
||||||
|
def end(
|
||||||
|
self,
|
||||||
|
inputs: dict[str, Any],
|
||||||
|
outputs: Dict[str, Any],
|
||||||
|
error: Exception | None = None,
|
||||||
|
metadata: dict[str, Any] | None = None,
|
||||||
|
):
|
||||||
|
self._run_tree.add_metadata({"inputs": inputs})
|
||||||
|
if metadata:
|
||||||
|
self._run_tree.add_metadata(metadata)
|
||||||
|
self._run_tree.end(outputs=outputs, error=self._error_to_string(error))
|
||||||
|
self._run_tree.post()
|
||||||
|
self._run_link = self._run_tree.get_url()
|
||||||
178
src/backend/base/langflow/services/tracing/langwatch.py
Normal file
178
src/backend/base/langflow/services/tracing/langwatch.py
Normal file
|
|
@ -0,0 +1,178 @@
|
||||||
|
from typing import TYPE_CHECKING, Any, Dict, Optional, cast
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
import nanoid # type: ignore
|
||||||
|
from langwatch.tracer import ContextSpan
|
||||||
|
from loguru import logger
|
||||||
|
|
||||||
|
from langflow.schema.data import Data
|
||||||
|
from langflow.services.tracing.base import BaseTracer
|
||||||
|
from langflow.services.tracing.schema import Log
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from langwatch.tracer import ContextSpan
|
||||||
|
from langwatch.types import SpanTypes
|
||||||
|
|
||||||
|
from langflow.graph.vertex.base import Vertex
|
||||||
|
|
||||||
|
|
||||||
|
class LangWatchTracer(BaseTracer):
|
||||||
|
flow_id: str
|
||||||
|
|
||||||
|
def __init__(self, trace_name: str, trace_type: str, project_name: str, trace_id: UUID):
|
||||||
|
self.trace_name = trace_name
|
||||||
|
self.trace_type = trace_type
|
||||||
|
self.project_name = project_name
|
||||||
|
self.trace_id = trace_id
|
||||||
|
self.flow_id = trace_name.split(" - ")[-1]
|
||||||
|
|
||||||
|
try:
|
||||||
|
self._ready = self.setup_langwatch()
|
||||||
|
|
||||||
|
# import after setting up langwatch so we are sure to be available
|
||||||
|
import nanoid # type: ignore
|
||||||
|
|
||||||
|
self.trace = self._client.trace(
|
||||||
|
trace_id=str(self.trace_id),
|
||||||
|
)
|
||||||
|
self.spans: dict[str, "ContextSpan"] = {}
|
||||||
|
|
||||||
|
name_without_id = " - ".join(trace_name.split(" - ")[0:-1])
|
||||||
|
self.trace.root_span.update(
|
||||||
|
span_id=f"{self.flow_id}-{nanoid.generate(size=6)}", # nanoid to make the span_id globally unique, which is required for LangWatch for now
|
||||||
|
name=name_without_id,
|
||||||
|
type=self._convert_trace_type(trace_type),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
logger.debug(f"Error setting up LangWatch tracer: {e}")
|
||||||
|
self._ready = False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def ready(self):
|
||||||
|
return self._ready
|
||||||
|
|
||||||
|
def setup_langwatch(self):
|
||||||
|
try:
|
||||||
|
import langwatch
|
||||||
|
|
||||||
|
self._client = langwatch
|
||||||
|
except ImportError:
|
||||||
|
logger.error("Could not import langwatch. Please install it with `pip install langwatch`.")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
def _convert_trace_type(self, trace_type: str):
|
||||||
|
trace_type_: "SpanTypes" = (
|
||||||
|
cast("SpanTypes", trace_type)
|
||||||
|
if trace_type in ["span", "llm", "chain", "tool", "agent", "guardrail", "rag"]
|
||||||
|
else "span"
|
||||||
|
)
|
||||||
|
return trace_type_
|
||||||
|
|
||||||
|
def add_trace(
|
||||||
|
self,
|
||||||
|
trace_id: str,
|
||||||
|
trace_name: str,
|
||||||
|
trace_type: str,
|
||||||
|
inputs: Dict[str, Any],
|
||||||
|
metadata: Dict[str, Any] | None = None,
|
||||||
|
vertex: Optional["Vertex"] = None,
|
||||||
|
):
|
||||||
|
# If user is not using session_id, then it becomes the same as flow_id, but
|
||||||
|
# we don't want to have an infinite thread with all the flow messages
|
||||||
|
if "session_id" in inputs and inputs["session_id"] != self.flow_id:
|
||||||
|
self.trace.update(metadata=(self.trace.metadata or {}) | {"thread_id": inputs["session_id"]})
|
||||||
|
|
||||||
|
name_without_id = " (".join(trace_name.split(" (")[0:-1])
|
||||||
|
|
||||||
|
trace_type_ = self._convert_trace_type(trace_type)
|
||||||
|
self.spans[trace_id] = self.trace.span(
|
||||||
|
span_id=f"{trace_id}-{nanoid.generate(size=6)}", # Add a nanoid to make the span_id globally unique, which is required for LangWatch for now
|
||||||
|
name=name_without_id,
|
||||||
|
type=trace_type_,
|
||||||
|
parent=(
|
||||||
|
[span for key, span in self.spans.items() for edge in vertex.incoming_edges if key == edge.source_id][
|
||||||
|
-1
|
||||||
|
]
|
||||||
|
if vertex and len(vertex.incoming_edges) > 0
|
||||||
|
else self.trace.root_span
|
||||||
|
),
|
||||||
|
input=self._convert_to_langwatch_types(inputs),
|
||||||
|
)
|
||||||
|
|
||||||
|
if trace_type_ == "llm" and "model_name" in inputs:
|
||||||
|
self.spans[trace_id].update(model=inputs["model_name"])
|
||||||
|
|
||||||
|
def end_trace(
|
||||||
|
self,
|
||||||
|
trace_id: str,
|
||||||
|
trace_name: str,
|
||||||
|
outputs: Dict[str, Any] | None = None,
|
||||||
|
error: Exception | None = None,
|
||||||
|
logs: list[Log | dict] = [],
|
||||||
|
):
|
||||||
|
if self.spans.get(trace_id):
|
||||||
|
# Workaround for when model is used just as a component not actually called as an LLM,
|
||||||
|
# to prevent LangWatch from calculating the cost based on it when it was in fact never called
|
||||||
|
if (
|
||||||
|
self.spans[trace_id].type == "llm"
|
||||||
|
and outputs
|
||||||
|
and "model_output" in outputs
|
||||||
|
and "text_output" not in outputs
|
||||||
|
):
|
||||||
|
self.spans[trace_id].update(metrics={"prompt_tokens": 0, "completion_tokens": 0})
|
||||||
|
|
||||||
|
self.spans[trace_id].end(output=self._convert_to_langwatch_types(outputs), error=error)
|
||||||
|
|
||||||
|
def end(
|
||||||
|
self,
|
||||||
|
inputs: dict[str, Any],
|
||||||
|
outputs: Dict[str, Any],
|
||||||
|
error: Exception | None = None,
|
||||||
|
metadata: dict[str, Any] | None = None,
|
||||||
|
):
|
||||||
|
self.trace.root_span.end(
|
||||||
|
input=self._convert_to_langwatch_types(inputs),
|
||||||
|
output=self._convert_to_langwatch_types(outputs),
|
||||||
|
error=error,
|
||||||
|
)
|
||||||
|
|
||||||
|
if metadata and "flow_name" in metadata:
|
||||||
|
self.trace.update(metadata=(self.trace.metadata or {}) | {"labels": [f"Flow: {metadata['flow_name']}"]})
|
||||||
|
self.trace.deferred_send_spans()
|
||||||
|
|
||||||
|
def _convert_to_langwatch_types(self, io_dict: Optional[Dict[str, Any]]):
|
||||||
|
from langwatch.utils import autoconvert_typed_values
|
||||||
|
|
||||||
|
if io_dict is None:
|
||||||
|
return None
|
||||||
|
converted = {}
|
||||||
|
for key, value in io_dict.items():
|
||||||
|
converted[key] = self._convert_to_langwatch_type(value)
|
||||||
|
return autoconvert_typed_values(converted)
|
||||||
|
|
||||||
|
def _convert_to_langwatch_type(self, value):
|
||||||
|
from langwatch.langchain import langchain_message_to_chat_message, langchain_messages_to_chat_messages
|
||||||
|
|
||||||
|
from langflow.schema.message import BaseMessage, Message
|
||||||
|
|
||||||
|
if isinstance(value, dict):
|
||||||
|
for key, _value in value.copy().items():
|
||||||
|
_value = self._convert_to_langwatch_type(_value)
|
||||||
|
value[key] = _value
|
||||||
|
elif isinstance(value, list):
|
||||||
|
value = [self._convert_to_langwatch_type(v) for v in value]
|
||||||
|
elif isinstance(value, Message):
|
||||||
|
if "prompt" in value:
|
||||||
|
prompt = value.load_lc_prompt()
|
||||||
|
if len(prompt.input_variables) == 0 and all(isinstance(m, BaseMessage) for m in prompt.messages):
|
||||||
|
value = langchain_messages_to_chat_messages([cast(list[BaseMessage], prompt.messages)])
|
||||||
|
else:
|
||||||
|
value = cast(dict, value.load_lc_prompt())
|
||||||
|
elif value.sender:
|
||||||
|
value = langchain_message_to_chat_message(value.to_lc_message())
|
||||||
|
else:
|
||||||
|
value = cast(dict, value.to_lc_document())
|
||||||
|
elif isinstance(value, Data):
|
||||||
|
value = cast(dict, value.to_lc_document())
|
||||||
|
return value
|
||||||
|
|
@ -1,27 +1,23 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
import os
|
import os
|
||||||
import traceback
|
|
||||||
import types
|
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from contextlib import asynccontextmanager
|
from contextlib import asynccontextmanager
|
||||||
from datetime import datetime, timezone
|
from typing import TYPE_CHECKING, Any, Dict, Optional
|
||||||
from typing import TYPE_CHECKING, Any, Dict, Optional, cast
|
|
||||||
from uuid import UUID
|
from uuid import UUID
|
||||||
|
|
||||||
from loguru import logger
|
from loguru import logger
|
||||||
|
|
||||||
from langflow.schema.data import Data
|
|
||||||
from langflow.services.base import Service
|
from langflow.services.base import Service
|
||||||
from langflow.services.tracing.base import BaseTracer
|
from langflow.services.tracing.base import BaseTracer
|
||||||
|
from langflow.services.tracing.langsmith import LangSmithTracer
|
||||||
|
from langflow.services.tracing.langwatch import LangWatchTracer
|
||||||
from langflow.services.tracing.schema import Log
|
from langflow.services.tracing.schema import Log
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from langflow.services.monitor.service import MonitorService
|
|
||||||
from langflow.services.settings.service import SettingsService
|
|
||||||
from langflow.custom.custom_component.component import Component
|
from langflow.custom.custom_component.component import Component
|
||||||
from langflow.graph.vertex.base import Vertex
|
from langflow.graph.vertex.base import Vertex
|
||||||
from langwatch.tracer import ContextSpan
|
from langflow.services.monitor.service import MonitorService
|
||||||
from langwatch.types import SpanTypes
|
from langflow.services.settings.service import SettingsService
|
||||||
|
|
||||||
|
|
||||||
class TracingService(Service):
|
class TracingService(Service):
|
||||||
|
|
@ -221,305 +217,3 @@ class TracingService(Service):
|
||||||
if "api_key" in key:
|
if "api_key" in key:
|
||||||
inputs[key] = "*****" # avoid logging api_keys for security reasons
|
inputs[key] = "*****" # avoid logging api_keys for security reasons
|
||||||
return inputs
|
return inputs
|
||||||
|
|
||||||
|
|
||||||
class LangSmithTracer(BaseTracer):
|
|
||||||
def __init__(self, trace_name: str, trace_type: str, project_name: str, trace_id: UUID):
|
|
||||||
from langsmith.run_trees import RunTree
|
|
||||||
|
|
||||||
self.trace_name = trace_name
|
|
||||||
self.trace_type = trace_type
|
|
||||||
self.project_name = project_name
|
|
||||||
self.trace_id = trace_id
|
|
||||||
try:
|
|
||||||
self._run_tree = RunTree(
|
|
||||||
project_name=self.project_name,
|
|
||||||
name=self.trace_name,
|
|
||||||
run_type=self.trace_type,
|
|
||||||
id=self.trace_id,
|
|
||||||
)
|
|
||||||
self._run_tree.add_event({"name": "Start", "time": datetime.now(timezone.utc).isoformat()})
|
|
||||||
self._children: dict[str, RunTree] = {}
|
|
||||||
self._ready = self.setup_langsmith()
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Error setting up LangSmith tracer: {e}")
|
|
||||||
self._ready = False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ready(self):
|
|
||||||
return self._ready
|
|
||||||
|
|
||||||
def setup_langsmith(self):
|
|
||||||
try:
|
|
||||||
from langsmith import Client
|
|
||||||
|
|
||||||
self._client = Client()
|
|
||||||
except ImportError:
|
|
||||||
logger.error("Could not import langsmith. Please install it with `pip install langsmith`.")
|
|
||||||
return False
|
|
||||||
os.environ["LANGCHAIN_TRACING_V2"] = "true"
|
|
||||||
return True
|
|
||||||
|
|
||||||
def add_trace(
|
|
||||||
self,
|
|
||||||
trace_id: str,
|
|
||||||
trace_name: str,
|
|
||||||
trace_type: str,
|
|
||||||
inputs: Dict[str, Any],
|
|
||||||
metadata: Dict[str, Any] | None = None,
|
|
||||||
vertex: Optional["Vertex"] = None,
|
|
||||||
):
|
|
||||||
if not self._ready:
|
|
||||||
return
|
|
||||||
processed_inputs = {}
|
|
||||||
if inputs:
|
|
||||||
processed_inputs = self._convert_to_langchain_types(inputs)
|
|
||||||
child = self._run_tree.create_child(
|
|
||||||
name=trace_name,
|
|
||||||
run_type=trace_type, # type: ignore[arg-type]
|
|
||||||
inputs=processed_inputs,
|
|
||||||
)
|
|
||||||
if metadata:
|
|
||||||
child.add_metadata(self._convert_to_langchain_types(metadata))
|
|
||||||
self._children[trace_name] = child
|
|
||||||
self._child_link: dict[str, str] = {}
|
|
||||||
|
|
||||||
def _convert_to_langchain_types(self, io_dict: Dict[str, Any]):
|
|
||||||
converted = {}
|
|
||||||
for key, value in io_dict.items():
|
|
||||||
converted[key] = self._convert_to_langchain_type(value)
|
|
||||||
return converted
|
|
||||||
|
|
||||||
def _convert_to_langchain_type(self, value):
|
|
||||||
from langflow.schema.message import Message
|
|
||||||
|
|
||||||
if isinstance(value, dict):
|
|
||||||
for key, _value in value.copy().items():
|
|
||||||
_value = self._convert_to_langchain_type(_value)
|
|
||||||
value[key] = _value
|
|
||||||
elif isinstance(value, list):
|
|
||||||
value = [self._convert_to_langchain_type(v) for v in value]
|
|
||||||
elif isinstance(value, Message):
|
|
||||||
if "prompt" in value:
|
|
||||||
value = value.load_lc_prompt()
|
|
||||||
elif value.sender:
|
|
||||||
value = value.to_lc_message()
|
|
||||||
else:
|
|
||||||
value = value.to_lc_document()
|
|
||||||
elif isinstance(value, Data):
|
|
||||||
value = value.to_lc_document()
|
|
||||||
elif isinstance(value, types.GeneratorType):
|
|
||||||
# generator is not serializable, also we can't consume it
|
|
||||||
value = str(value)
|
|
||||||
return value
|
|
||||||
|
|
||||||
def end_trace(
|
|
||||||
self,
|
|
||||||
trace_id: str,
|
|
||||||
trace_name: str,
|
|
||||||
outputs: Dict[str, Any] | None = None,
|
|
||||||
error: Exception | None = None,
|
|
||||||
logs: list[Log | dict] = [],
|
|
||||||
):
|
|
||||||
child = self._children[trace_name]
|
|
||||||
raw_outputs = {}
|
|
||||||
processed_outputs = {}
|
|
||||||
if outputs:
|
|
||||||
raw_outputs = outputs
|
|
||||||
processed_outputs = self._convert_to_langchain_types(outputs)
|
|
||||||
if logs:
|
|
||||||
child.add_metadata(self._convert_to_langchain_types({"logs": {log.get("name"): log for log in logs}}))
|
|
||||||
child.add_metadata(self._convert_to_langchain_types({"outputs": raw_outputs}))
|
|
||||||
child.end(outputs=processed_outputs, error=self._error_to_string(error))
|
|
||||||
if error:
|
|
||||||
child.patch()
|
|
||||||
else:
|
|
||||||
child.post()
|
|
||||||
self._child_link[trace_name] = child.get_url()
|
|
||||||
|
|
||||||
def _error_to_string(self, error: Optional[Exception]):
|
|
||||||
error_message = None
|
|
||||||
if error:
|
|
||||||
string_stacktrace = traceback.format_exception(error)
|
|
||||||
error_message = f"{error.__class__.__name__}: {error}\n\n{string_stacktrace}"
|
|
||||||
return error_message
|
|
||||||
|
|
||||||
def end(
|
|
||||||
self,
|
|
||||||
inputs: dict[str, Any],
|
|
||||||
outputs: Dict[str, Any],
|
|
||||||
error: Exception | None = None,
|
|
||||||
metadata: dict[str, Any] | None = None,
|
|
||||||
):
|
|
||||||
self._run_tree.add_metadata({"inputs": inputs})
|
|
||||||
if metadata:
|
|
||||||
self._run_tree.add_metadata(metadata)
|
|
||||||
self._run_tree.end(outputs=outputs, error=self._error_to_string(error))
|
|
||||||
self._run_tree.post()
|
|
||||||
self._run_link = self._run_tree.get_url()
|
|
||||||
|
|
||||||
|
|
||||||
class LangWatchTracer(BaseTracer):
|
|
||||||
flow_id: str
|
|
||||||
|
|
||||||
def __init__(self, trace_name: str, trace_type: str, project_name: str, trace_id: UUID):
|
|
||||||
self.trace_name = trace_name
|
|
||||||
self.trace_type = trace_type
|
|
||||||
self.project_name = project_name
|
|
||||||
self.trace_id = trace_id
|
|
||||||
self.flow_id = trace_name.split(" - ")[-1]
|
|
||||||
|
|
||||||
try:
|
|
||||||
self._ready = self.setup_langwatch()
|
|
||||||
|
|
||||||
# import after setting up langwatch so we are sure to be available
|
|
||||||
import nanoid # type: ignore
|
|
||||||
|
|
||||||
self.trace = self._client.trace(
|
|
||||||
trace_id=str(self.trace_id),
|
|
||||||
)
|
|
||||||
self.spans: dict[str, "ContextSpan"] = {}
|
|
||||||
|
|
||||||
name_without_id = " - ".join(trace_name.split(" - ")[0:-1])
|
|
||||||
self.trace.root_span.update(
|
|
||||||
span_id=f"{self.flow_id}-{nanoid.generate(size=6)}", # nanoid to make the span_id globally unique, which is required for LangWatch for now
|
|
||||||
name=name_without_id,
|
|
||||||
type=self._convert_trace_type(trace_type),
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"Error setting up LangWatch tracer: {e}")
|
|
||||||
self._ready = False
|
|
||||||
|
|
||||||
@property
|
|
||||||
def ready(self):
|
|
||||||
return self._ready
|
|
||||||
|
|
||||||
def setup_langwatch(self):
|
|
||||||
try:
|
|
||||||
import langwatch
|
|
||||||
|
|
||||||
self._client = langwatch
|
|
||||||
except ImportError:
|
|
||||||
logger.error("Could not import langwatch. Please install it with `pip install langwatch`.")
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
def _convert_trace_type(self, trace_type: str):
|
|
||||||
trace_type_: "SpanTypes" = (
|
|
||||||
cast("SpanTypes", trace_type)
|
|
||||||
if trace_type in ["span", "llm", "chain", "tool", "agent", "guardrail", "rag"]
|
|
||||||
else "span"
|
|
||||||
)
|
|
||||||
return trace_type_
|
|
||||||
|
|
||||||
def add_trace(
|
|
||||||
self,
|
|
||||||
trace_id: str,
|
|
||||||
trace_name: str,
|
|
||||||
trace_type: str,
|
|
||||||
inputs: Dict[str, Any],
|
|
||||||
metadata: Dict[str, Any] | None = None,
|
|
||||||
vertex: Optional["Vertex"] = None,
|
|
||||||
):
|
|
||||||
import nanoid
|
|
||||||
|
|
||||||
# If user is not using session_id, then it becomes the same as flow_id, but
|
|
||||||
# we don't want to have an infinite thread with all the flow messages
|
|
||||||
if "session_id" in inputs and inputs["session_id"] != self.flow_id:
|
|
||||||
self.trace.update(metadata=(self.trace.metadata or {}) | {"thread_id": inputs["session_id"]})
|
|
||||||
|
|
||||||
name_without_id = " (".join(trace_name.split(" (")[0:-1])
|
|
||||||
|
|
||||||
trace_type_ = self._convert_trace_type(trace_type)
|
|
||||||
self.spans[trace_id] = self.trace.span(
|
|
||||||
span_id=f"{trace_id}-{nanoid.generate(size=6)}", # Add a nanoid to make the span_id globally unique, which is required for LangWatch for now
|
|
||||||
name=name_without_id,
|
|
||||||
type=trace_type_,
|
|
||||||
parent=(
|
|
||||||
[span for key, span in self.spans.items() for edge in vertex.incoming_edges if key == edge.source_id][
|
|
||||||
-1
|
|
||||||
]
|
|
||||||
if vertex and len(vertex.incoming_edges) > 0
|
|
||||||
else self.trace.root_span
|
|
||||||
),
|
|
||||||
input=self._convert_to_langwatch_types(inputs),
|
|
||||||
)
|
|
||||||
|
|
||||||
if trace_type_ == "llm" and "model_name" in inputs:
|
|
||||||
self.spans[trace_id].update(model=inputs["model_name"])
|
|
||||||
|
|
||||||
def end_trace(
|
|
||||||
self,
|
|
||||||
trace_id: str,
|
|
||||||
trace_name: str,
|
|
||||||
outputs: Dict[str, Any] | None = None,
|
|
||||||
error: Exception | None = None,
|
|
||||||
logs: list[Log | dict] = [],
|
|
||||||
):
|
|
||||||
if self.spans.get(trace_id):
|
|
||||||
# Workaround for when model is used just as a component not actually called as an LLM,
|
|
||||||
# to prevent LangWatch from calculating the cost based on it when it was in fact never called
|
|
||||||
if (
|
|
||||||
self.spans[trace_id].type == "llm"
|
|
||||||
and outputs
|
|
||||||
and "model_output" in outputs
|
|
||||||
and "text_output" not in outputs
|
|
||||||
):
|
|
||||||
self.spans[trace_id].update(metrics={"prompt_tokens": 0, "completion_tokens": 0})
|
|
||||||
|
|
||||||
self.spans[trace_id].end(output=self._convert_to_langwatch_types(outputs), error=error)
|
|
||||||
|
|
||||||
def end(
|
|
||||||
self,
|
|
||||||
inputs: dict[str, Any],
|
|
||||||
outputs: Dict[str, Any],
|
|
||||||
error: Exception | None = None,
|
|
||||||
metadata: dict[str, Any] | None = None,
|
|
||||||
):
|
|
||||||
self.trace.root_span.end(
|
|
||||||
input=self._convert_to_langwatch_types(inputs),
|
|
||||||
output=self._convert_to_langwatch_types(outputs),
|
|
||||||
error=error,
|
|
||||||
)
|
|
||||||
|
|
||||||
if metadata and "flow_name" in metadata:
|
|
||||||
self.trace.update(metadata=(self.trace.metadata or {}) | {"labels": [f"Flow: {metadata['flow_name']}"]})
|
|
||||||
self.trace.deferred_send_spans()
|
|
||||||
|
|
||||||
def _convert_to_langwatch_types(self, io_dict: Optional[Dict[str, Any]]):
|
|
||||||
from langwatch.utils import autoconvert_typed_values
|
|
||||||
|
|
||||||
if io_dict is None:
|
|
||||||
return None
|
|
||||||
converted = {}
|
|
||||||
for key, value in io_dict.items():
|
|
||||||
converted[key] = self._convert_to_langwatch_type(value)
|
|
||||||
return autoconvert_typed_values(converted)
|
|
||||||
|
|
||||||
def _convert_to_langwatch_type(self, value):
|
|
||||||
from langflow.schema.message import Message, BaseMessage
|
|
||||||
from langwatch.langchain import (
|
|
||||||
langchain_messages_to_chat_messages,
|
|
||||||
langchain_message_to_chat_message,
|
|
||||||
)
|
|
||||||
|
|
||||||
if isinstance(value, dict):
|
|
||||||
for key, _value in value.copy().items():
|
|
||||||
_value = self._convert_to_langwatch_type(_value)
|
|
||||||
value[key] = _value
|
|
||||||
elif isinstance(value, list):
|
|
||||||
value = [self._convert_to_langwatch_type(v) for v in value]
|
|
||||||
elif isinstance(value, Message):
|
|
||||||
if "prompt" in value:
|
|
||||||
prompt = value.load_lc_prompt()
|
|
||||||
if len(prompt.input_variables) == 0 and all(isinstance(m, BaseMessage) for m in prompt.messages):
|
|
||||||
value = langchain_messages_to_chat_messages([cast(list[BaseMessage], prompt.messages)])
|
|
||||||
else:
|
|
||||||
value = cast(dict, value.load_lc_prompt())
|
|
||||||
elif value.sender:
|
|
||||||
value = langchain_message_to_chat_message(value.to_lc_message())
|
|
||||||
else:
|
|
||||||
value = cast(dict, value.to_lc_document())
|
|
||||||
elif isinstance(value, Data):
|
|
||||||
value = cast(dict, value.to_lc_document())
|
|
||||||
return value
|
|
||||||
|
|
|
||||||
2429
src/backend/base/poetry.lock
generated
2429
src/backend/base/poetry.lock
generated
File diff suppressed because it is too large
Load diff
|
|
@ -71,9 +71,7 @@ opentelemetry-sdk = "^1.25.0"
|
||||||
opentelemetry-exporter-prometheus = "^0.46b0"
|
opentelemetry-exporter-prometheus = "^0.46b0"
|
||||||
prometheus-client = "^0.20.0"
|
prometheus-client = "^0.20.0"
|
||||||
aiofiles = "^24.1.0"
|
aiofiles = "^24.1.0"
|
||||||
crewai = "^0.36.0"
|
|
||||||
setuptools = ">=70"
|
setuptools = ">=70"
|
||||||
langwatch = "^0.1.3"
|
|
||||||
|
|
||||||
[tool.poetry.extras]
|
[tool.poetry.extras]
|
||||||
deploy = ["celery", "redis", "flower"]
|
deploy = ["celery", "redis", "flower"]
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue