├── myscale_telemetry ├── __init__.py ├── span_data.py ├── consumer.py ├── task_manager.py └── handler.py ├── .gitignore ├── assets ├── dashboard.png ├── workflow.png ├── add_data_source.png ├── import_dashboard.png ├── config_data_source.png ├── clickhouse_data_source.png └── workflow.excalidraw ├── tests ├── .env.example └── test_callbacks.py ├── requirements.txt ├── .pylintrc ├── deploy ├── volumes │ └── config │ │ └── users.d │ │ └── custom_users_config.xml └── docker-compose.yaml ├── .pre-commit-config.yaml ├── setup.py ├── LICENSE ├── README.md └── dashboard └── grafana_myscale_trace_dashboard.json /myscale_telemetry/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | .env 3 | __pycache__ 4 | .idea 5 | *.egg-info 6 | dist 7 | build -------------------------------------------------------------------------------- /assets/dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myscale/myscale-telemetry/HEAD/assets/dashboard.png -------------------------------------------------------------------------------- /assets/workflow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myscale/myscale-telemetry/HEAD/assets/workflow.png -------------------------------------------------------------------------------- /assets/add_data_source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myscale/myscale-telemetry/HEAD/assets/add_data_source.png -------------------------------------------------------------------------------- /assets/import_dashboard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myscale/myscale-telemetry/HEAD/assets/import_dashboard.png -------------------------------------------------------------------------------- /assets/config_data_source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myscale/myscale-telemetry/HEAD/assets/config_data_source.png -------------------------------------------------------------------------------- /assets/clickhouse_data_source.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/myscale/myscale-telemetry/HEAD/assets/clickhouse_data_source.png -------------------------------------------------------------------------------- /tests/.env.example: -------------------------------------------------------------------------------- 1 | MYSCALE_HOST=localhost 2 | MYSCALE_PORT=8123 3 | MYSCALE_USERNAME=default 4 | MYSCALE_PASSWORD= 5 | 6 | OPENAI_API_KEY=sk- -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | backoff>=2.2.1 2 | langchain~=0.3.0 3 | langchain-community~=0.3.0 4 | langchain-openai~=0.2.0 5 | clickhouse-connect>=0.7 6 | tiktoken>=0.7.0 -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [pylint] 2 | good-names=id, df 3 | 4 | [MASTER] 5 | extension-pkg-allow-list=pydantic 6 | 7 | [MESSAGES CONTROL] 8 | disable=C0114,R0903,R0913,R0917,C0415,W0703,W0621,E1137,R0902,E0611 9 | 10 | [DESIGN] 11 | max-locals=20 12 | max-branches=20 13 | max-statements=60 14 | 15 | [SIMILARITIES] 16 | min-similarity-lines=10 17 | -------------------------------------------------------------------------------- /deploy/volumes/config/users.d/custom_users_config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | default 5 | 6 | ::/0 7 | 8 | default 9 | default 10 | 1 11 | 12 | 13 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: black 5 | name: black-formatting 6 | entry: black 7 | types: [python] 8 | exclude: ^main.py 9 | language: system 10 | - id: pylint 11 | name: pylint 12 | entry: pylint 13 | types: [python] 14 | exclude: (^main.py|docs/) 15 | language: system 16 | # - id: mypy 17 | # files: api/ 18 | # language_version: python3.9 19 | -------------------------------------------------------------------------------- /deploy/docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: '0.1' 2 | 3 | services: 4 | myscaledb: 5 | image: myscale/myscaledb:1.5 6 | tty: true 7 | ports: 8 | - '8123:8123' 9 | - '9000:9000' 10 | - '8998:8998' 11 | - '9363:9363' 12 | - '9116:9116' 13 | volumes: 14 | - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/data:/var/lib/clickhouse 15 | - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/log:/var/log/clickhouse-server 16 | - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/config/users.d/custom_users_config.xml:/etc/clickhouse-server/users.d/custom_users_config.xml 17 | deploy: 18 | resources: 19 | limits: 20 | cpus: "8.00" 21 | memory: 16Gb 22 | 23 | grafana: 24 | image: grafana/grafana:latest 25 | container_name: grafana 26 | ports: 27 | - '3000:3000' 28 | environment: 29 | - GF_SECURITY_ADMIN_PASSWORD=admin 30 | volumes: 31 | - ${DOCKER_VOLUME_DIRECTORY:-.}/volumes/grafana:/var/lib/grafana 32 | depends_on: 33 | - myscaledb -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from setuptools import setup, find_packages 3 | 4 | 5 | this_directory = Path(__file__).parent 6 | long_description = (this_directory / "README.md").read_text() 7 | 8 | setup( 9 | name="myscale-telemetry", 10 | version="0.3.2", 11 | description="Open-source observability for your LLM application.", 12 | long_description=long_description, 13 | long_description_content_type="text/markdown", 14 | author="Xu Jing", 15 | author_email="xuj@myscale.com", 16 | url="https://github.com/myscale/myscale-telemetry", 17 | packages=find_packages(), 18 | install_requires=[ 19 | "backoff>=2.2.1", 20 | "langchain~=0.3.0", 21 | "langchain-community~=0.3.0", 22 | "clickhouse-connect>=0.7", 23 | "langchain-openai~=0.2.0", 24 | "tiktoken>=0.7.0", 25 | ], 26 | classifiers=[ 27 | "Programming Language :: Python :: 3", 28 | "License :: OSI Approved :: MIT License", 29 | "Operating System :: OS Independent", 30 | ], 31 | python_requires=">=3.10", 32 | ) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 MyScale 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/test_callbacks.py: -------------------------------------------------------------------------------- 1 | import time 2 | import logging 3 | import logging.config 4 | from operator import itemgetter 5 | import dotenv 6 | from langchain_core.output_parsers import StrOutputParser 7 | from langchain_core.prompts import ChatPromptTemplate 8 | from langchain_core.runnables import RunnableConfig 9 | from langchain_openai import ChatOpenAI, OpenAIEmbeddings 10 | from langchain_community.vectorstores import MyScale 11 | from myscale_telemetry.handler import MyScaleCallbackHandler 12 | 13 | logging.config.fileConfig("logging.conf") 14 | 15 | 16 | def test_callback_handler(): 17 | """Test the MyScaleCallbackHandler""" 18 | dotenv.load_dotenv() 19 | # pylint: disable=no-member 20 | vectorstore = MyScale.from_texts( 21 | ["harrison worked at kensho"], embedding=OpenAIEmbeddings() 22 | ) 23 | # pylint: enable=no-member 24 | retriever = vectorstore.as_retriever() 25 | model = ChatOpenAI() 26 | template = """Answer the question based only on the following context: 27 | {context} 28 | 29 | Question: {question} 30 | 31 | """ 32 | prompt = ChatPromptTemplate.from_template(template) 33 | 34 | chain = ( 35 | { 36 | "context": itemgetter("question") | retriever, 37 | "question": itemgetter("question"), 38 | } 39 | | prompt 40 | | model 41 | | StrOutputParser() 42 | ) 43 | 44 | test_database = "trace_test" 45 | test_table = "traces" 46 | test_question = "where did harrison work" 47 | callback_handler = MyScaleCallbackHandler( 48 | database_name=test_database, table_name=test_table 49 | ) 50 | 51 | chain.invoke( 52 | {"question": test_question}, config=RunnableConfig(callbacks=[callback_handler]) 53 | ) 54 | 55 | time.sleep(5) 56 | trace_id = callback_handler.myscale_client.query( 57 | f"SELECT TraceId FROM {test_database}.{test_table} " 58 | f"WHERE SpanAttributes['question'] = '{test_question}' " 59 | f"AND ParentSpanId = '' order by StartTime DESC limit 1" 60 | ).result_columns[0][0] 61 | 62 | trace_root = callback_handler.myscale_client.query( 63 | f"SELECT * FROM {test_database}.{test_table} " 64 | f"WHERE TraceId = '{trace_id}' AND ParentSpanId = ''" 65 | ) 66 | logging.info( 67 | "Callback traces:\n%s", "\n".join(str(d) for d in trace_root.named_results()) 68 | ) 69 | 70 | assert ( 71 | callback_handler.myscale_client.query( 72 | f"SELECT count(*) FROM {test_database}.{test_table} WHERE TraceId = '{trace_id}' " 73 | f"AND StatusCode = 'STATUS_CODE_SUCCESS'" 74 | ).result_columns[0][0] 75 | == 9 76 | ) 77 | -------------------------------------------------------------------------------- /myscale_telemetry/span_data.py: -------------------------------------------------------------------------------- 1 | from uuid import UUID 2 | from datetime import datetime 3 | from typing import List, Optional, Any, Dict 4 | 5 | 6 | class SpanData: 7 | """Represents a span of data for tracing and monitoring. 8 | 9 | This class encapsulates the details of a span, which is 10 | a unit of work within a trace. Spans are used to track the 11 | timing, metadata, and status of various operations in a LLM application. 12 | 13 | Attributes: 14 | trace_id (UUID): The unique identifier for the trace this span belongs to. 15 | span_id (UUID): The unique identifier for this span. 16 | parent_span_id (Any): The identifier for the parent span, if any. 17 | start_time (datetime): The start time of the span. 18 | name (str): The name of the span, typically describing the operation. 19 | kind (str): The kind of span (e.g., llm, retriever). 20 | service_name (Optional[str]): The name of the service this span belongs to. 21 | end_time (Optional[str]): The end time of the span. 22 | span_attributes (Dict[str, str]): Attributes specific to this span (e.g., 23 | question, prompts, retrieved documents, llm results). 24 | resource_attributes (Dict[str, str]): Resource attributes specific to the 25 | span are derived from the serialized data. 26 | duration (Optional[int]): The duration of the span in microseconds. 27 | status_code (Optional[str]): The status code indicating the outcome of the span. 28 | status_message (Optional[str]): A message providing additional details about the status. 29 | """ 30 | 31 | def __init__( 32 | self, 33 | trace_id: UUID, 34 | span_id: UUID, 35 | parent_span_id: Any, 36 | start_time: datetime, 37 | name: str, 38 | kind: str, 39 | span_attributes: Dict[str, str], 40 | resource_attributes: Dict[str, str], 41 | service_name: Optional[str] = "LangChain", 42 | end_time: Optional[str] = None, 43 | duration: Optional[int] = None, 44 | status_code: Optional[str] = None, 45 | status_message: Optional[str] = None, 46 | ): 47 | self.trace_id = trace_id 48 | self.span_id = span_id 49 | self.parent_span_id = parent_span_id 50 | self.start_time = start_time 51 | self.name = name 52 | self.kind = kind 53 | self.service_name = service_name 54 | self.end_time = end_time 55 | self.span_attributes = span_attributes 56 | self.resource_attributes = resource_attributes 57 | self.duration = duration 58 | self.status_code = status_code 59 | self.status_message = status_message 60 | 61 | def update( 62 | self, 63 | end_time: datetime, 64 | span_attributes: Dict[str, str], 65 | status_code: str, 66 | status_message: Optional[str] = None, 67 | ) -> None: 68 | """Updates the end time, span attributes, status code, and status message of the span.""" 69 | self.end_time = end_time 70 | self.duration = int((self.end_time - self.start_time).total_seconds() * 1000000) 71 | self.span_attributes.update(span_attributes) 72 | self.status_code = status_code 73 | self.status_message = status_message 74 | 75 | def to_list(self) -> List[Any]: 76 | """Converts the span data into a list format for uploading.""" 77 | return [ 78 | str(self.trace_id) if self.trace_id is not None else "", 79 | str(self.span_id) if self.span_id is not None else "", 80 | str(self.parent_span_id) if self.parent_span_id is not None else "", 81 | self.start_time, 82 | self.end_time, 83 | self.duration, 84 | self.name, 85 | self.kind, 86 | self.service_name, 87 | self.span_attributes, 88 | self.resource_attributes, 89 | self.status_code, 90 | self.status_message, 91 | ] 92 | -------------------------------------------------------------------------------- /myscale_telemetry/consumer.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import time 3 | from queue import Empty, Queue 4 | from threading import Thread 5 | from typing import List, Any 6 | 7 | import backoff 8 | from clickhouse_connect.driver.client import Client 9 | 10 | 11 | class Consumer(Thread): 12 | """A consumer thread that uploads data to the MyScale vector database. 13 | 14 | This class is responsible for consuming data from a queue and uploading it to the MyScale 15 | database in batches. It runs as a separate thread to ensure that data is uploaded 16 | asynchronously without blocking the main application. 17 | 18 | Attributes: 19 | _log (Logger): The logger for the consumer thread. 20 | _identifier (int): A unique identifier for the consumer thread. 21 | _queue (Queue): The queue from which to consume data. 22 | _client (Client): The MyScale database client for uploading data. 23 | _upload_interval (float): The interval between uploads in seconds. 24 | _max_retries (int): The maximum number of retries for uploading data. 25 | _max_batch_size (int): The maximum batch size for uploading data. 26 | _database_name (str): The name of the database to use. 27 | _table_name (str): The name of the table to use. 28 | """ 29 | 30 | _log = logging.getLogger(__name__) 31 | _identifier: int 32 | _queue: Queue 33 | _client: Client 34 | _upload_interval: float 35 | _max_retries: int 36 | _max_batch_size: int 37 | _database_name: str 38 | _table_name: str 39 | 40 | def __init__( 41 | self, 42 | identifier: int, 43 | queue: Queue, 44 | client: Client, 45 | upload_interval: float, 46 | max_retries: int, 47 | max_batch_size: int, 48 | database_name: str, 49 | table_name: str, 50 | ) -> None: 51 | """Initialize the consumer thread.""" 52 | Thread.__init__(self, daemon=True) 53 | self._identifier = identifier 54 | self.running = True 55 | self._queue = queue 56 | self._client = client 57 | self._upload_interval = upload_interval 58 | self._max_retries = max_retries 59 | self._max_batch_size = max_batch_size 60 | self._database_name = database_name 61 | self._table_name = table_name 62 | 63 | self._log.debug("start creating trace database and table if not exists") 64 | self._client.command(f"""CREATE DATABASE IF NOT EXISTS {self._database_name}""") 65 | self._client.command( 66 | f"""CREATE TABLE IF NOT EXISTS {self._database_name}.{self._table_name} 67 | ( 68 | `TraceId` String CODEC(ZSTD(1)), 69 | `SpanId` String CODEC(ZSTD(1)), 70 | `ParentSpanId` String CODEC(ZSTD(1)), 71 | `StartTime` DateTime64(9) CODEC(Delta(8), ZSTD(1)), 72 | `EndTime` DateTime64(9) CODEC(Delta(8), ZSTD(1)), 73 | `Duration` Int64 CODEC(ZSTD(1)), 74 | `SpanName` LowCardinality(String) CODEC(ZSTD(1)), 75 | `SpanKind` LowCardinality(String) CODEC(ZSTD(1)), 76 | `ServiceName` LowCardinality(String) CODEC(ZSTD(1)), 77 | `SpanAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)), 78 | `ResourceAttributes` Map(LowCardinality(String), String) CODEC(ZSTD(1)), 79 | `StatusCode` LowCardinality(String) CODEC(ZSTD(1)), 80 | `StatusMessage` String CODEC(ZSTD(1)), 81 | INDEX idx_trace_id TraceId TYPE bloom_filter(0.001) GRANULARITY 1, 82 | INDEX idx_res_attr_key mapKeys(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, 83 | INDEX idx_res_attr_value mapValues(ResourceAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, 84 | INDEX idx_span_attr_key mapKeys(SpanAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, 85 | INDEX idx_span_attr_value mapValues(SpanAttributes) TYPE bloom_filter(0.01) GRANULARITY 1, 86 | INDEX idx_duration Duration TYPE minmax GRANULARITY 1 87 | ) 88 | ENGINE = MergeTree() 89 | PARTITION BY toDate(StartTime) 90 | ORDER BY (SpanName, toUnixTimestamp(StartTime), TraceId) 91 | SETTINGS index_granularity = 8192""" 92 | ) 93 | 94 | def run(self) -> None: 95 | """Run the consumer thread.""" 96 | self._log.debug("consumer %s is running...", self._identifier) 97 | while self.running: 98 | self.upload() 99 | time.sleep(self._upload_interval) 100 | 101 | self.upload() 102 | self._log.debug("consumer %s stopped", self._identifier) 103 | 104 | def stop(self) -> None: 105 | """Stop the consumer.""" 106 | self._log.debug("stop consumer %s", self._identifier) 107 | self.running = False 108 | 109 | def get_batch(self) -> List[Any]: 110 | """Return a batch of items if exists""" 111 | items = [] 112 | 113 | while len(items) < self._max_batch_size: 114 | try: 115 | item = self._queue.get_nowait() 116 | items.append(item) 117 | except Empty: 118 | break 119 | 120 | return items 121 | 122 | def upload(self) -> None: 123 | """Upload a batch of items to MyScale, return whether successful.""" 124 | 125 | batch_data = self.get_batch() 126 | if len(batch_data) == 0: 127 | return 128 | 129 | try: 130 | self.upload_batch(batch_data) 131 | except Exception as e: 132 | self._log.exception("error uploading data to MyScale: %s", e) 133 | finally: 134 | for _ in batch_data: 135 | self._queue.task_done() 136 | 137 | def upload_batch(self, batch_data: List[Any]) -> None: 138 | """Upload a batch of items to MyScale with retries.""" 139 | self._log.debug("uploading batch data: %s to MyScale", batch_data) 140 | 141 | @backoff.on_exception(backoff.expo, Exception, max_tries=self._max_retries) 142 | def insert_with_backoff(batch_data_: List[Any]): 143 | return self._client.insert( 144 | table=self._table_name, database=self._database_name, data=batch_data_ 145 | ) 146 | 147 | insert_with_backoff(batch_data) 148 | self._log.debug("successfully uploaded batch of %d items", len(batch_data)) 149 | -------------------------------------------------------------------------------- /myscale_telemetry/task_manager.py: -------------------------------------------------------------------------------- 1 | import atexit 2 | import logging 3 | from contextvars import ContextVar 4 | from uuid import uuid4, UUID 5 | from typing import List, Optional, Any, Dict 6 | from queue import Queue 7 | from datetime import datetime 8 | from clickhouse_connect.driver.client import Client 9 | 10 | from .consumer import Consumer 11 | from .span_data import SpanData 12 | 13 | 14 | def create_uuid() -> UUID: 15 | """Create a new UUID.""" 16 | return uuid4() 17 | 18 | 19 | class TaskManager: 20 | """Manages the tasks of uploading data to the MyScale vector database. 21 | 22 | This class is responsible for managing a queue of tasks that upload span data to the MyScale 23 | database. It uses a pool of consumer threads to process the tasks asynchronously. 24 | 25 | Attributes: 26 | _log (Logger): The logger for the task manager. 27 | _consumers (List[Consumer]): The list of consumer threads. 28 | _client (Client): The MyScale database client. 29 | _threads (int): The number of consumer threads. 30 | _max_retries (int): The maximum number of retries for uploading data. 31 | _max_batch_size (int): The maximum batch size for uploading data. 32 | _upload_interval (float): The interval between uploads in seconds. 33 | _queue (Queue): The queue of tasks. 34 | _database_name (str): The name of the database to use. 35 | _table_name (str): The name of the table to use. 36 | """ 37 | 38 | _log = logging.getLogger(__name__) 39 | _consumers: List[Consumer] 40 | _client: Client 41 | _threads: int 42 | _max_retries: int 43 | _max_batch_size: int 44 | _upload_interval: float 45 | _queue: Queue 46 | _database_name: str 47 | _table_name: str 48 | 49 | def __init__( 50 | self, 51 | client: Client, 52 | threads: int, 53 | max_retries: int, 54 | max_batch_size: int, 55 | max_task_queue_size: int, 56 | upload_interval: float, 57 | database_name: str, 58 | table_name: str, 59 | ) -> None: 60 | """Initializes the TaskManager with the MyScale database client and other parameters. 61 | 62 | Parameters: 63 | client (Client): The MyScale database client. 64 | threads (int): The number of consumer threads. 65 | max_retries (int): The maximum number of retries for uploading data. 66 | max_batch_size (int): The maximum batch size for uploading data. 67 | max_task_queue_size (int): The maximum size of the task queue. 68 | upload_interval (float): The interval between uploads in seconds. 69 | database_name (str): The name of the database to use. 70 | table_name (str): The name of the table to use. 71 | """ 72 | self._client = client 73 | self._threads = threads 74 | self._max_retries = max_retries 75 | self._max_batch_size = max_batch_size 76 | self._upload_interval = upload_interval 77 | self._queue = Queue(max_task_queue_size) 78 | self._database_name = database_name 79 | self._table_name = table_name 80 | self.spans = {} 81 | self._consumers = [] 82 | self.trace_id = ContextVar[UUID]("trace_id", default=None) 83 | self.__init_consumers() 84 | atexit.register(self.join) 85 | 86 | def __init_consumers(self) -> None: 87 | """Initialize the consumer threads.""" 88 | for i in range(self._threads): 89 | consumer = Consumer( 90 | identifier=i, 91 | queue=self._queue, 92 | client=self._client, 93 | upload_interval=self._upload_interval, 94 | max_retries=self._max_retries, 95 | max_batch_size=self._max_batch_size, 96 | database_name=self._database_name, 97 | table_name=self._table_name, 98 | ) 99 | consumer.start() 100 | self._consumers.append(consumer) 101 | 102 | def create_trace_id(self) -> UUID: 103 | """Create a new trace ID.""" 104 | trace_id = create_uuid() 105 | self.trace_id.set(trace_id) 106 | return trace_id 107 | 108 | def get_trace_id(self) -> Any: 109 | """Get the current trace ID.""" 110 | return self.trace_id.get() 111 | 112 | def create_span( 113 | self, 114 | trace_id: UUID, 115 | span_id: UUID, 116 | parent_span_id: Any, 117 | start_time: datetime, 118 | name: str, 119 | kind: str, 120 | span_attributes: Dict[str, str], 121 | resource_attributes: Dict[str, str], 122 | ) -> None: 123 | """Create a new span data object.""" 124 | if span_id in self.spans: 125 | raise ValueError(f"Span with {span_id} id already exists") 126 | 127 | self.spans[span_id] = SpanData( 128 | trace_id=trace_id, 129 | span_id=span_id, 130 | parent_span_id=parent_span_id, 131 | start_time=start_time, 132 | name=name, 133 | kind=kind, 134 | span_attributes=span_attributes, 135 | resource_attributes=resource_attributes, 136 | ) 137 | 138 | def end_span( 139 | self, 140 | span_id: UUID, 141 | end_time: datetime, 142 | span_attributes: Dict[str, str], 143 | status_code: str, 144 | status_message: Optional[str] = None, 145 | force_count_tokens: bool = False, 146 | ) -> None: 147 | """End a span and add its data to the queue.""" 148 | if span_id not in self.spans: 149 | raise ValueError(f"Span with {span_id} id not exists") 150 | 151 | if force_count_tokens: 152 | span_attributes["total_tokens"] = str( 153 | int(span_attributes.get("completion_tokens", 0)) 154 | + int(self.spans[span_id].span_attributes.get("prompt_tokens", 0)) 155 | ) 156 | 157 | self.spans[span_id].update( 158 | end_time, span_attributes, status_code, status_message 159 | ) 160 | self.__add_span_data(self.spans[span_id].to_list()) 161 | del self.spans[span_id] 162 | 163 | def __add_span_data(self, data: List[Any]) -> None: 164 | """Add span data to the trace queue, return whether successful.""" 165 | self._log.debug("adding span data: %s", data) 166 | self._queue.put_nowait(data) 167 | 168 | def flush(self) -> None: 169 | """Flush all data from the internal queue to MyScale.""" 170 | self._log.debug("flushing queue") 171 | size = self._queue.qsize() 172 | self._queue.join() 173 | 174 | self._log.debug( 175 | "successfully flushed about %s span items.", 176 | size, 177 | ) 178 | 179 | def join(self) -> None: 180 | """Join all consumer threads.""" 181 | self._log.debug("joining %s consumer threads", len(self._consumers)) 182 | for consumer in self._consumers: 183 | consumer.stop() 184 | try: 185 | consumer.join() 186 | except RuntimeError: 187 | # consumer thread has not started 188 | pass 189 | 190 | self._log.debug( 191 | "consumer thread %d joined", 192 | consumer._identifier, # pylint: disable=protected-access 193 | ) 194 | 195 | def shutdown(self) -> None: 196 | """Shut down the TaskManager and flush all data.""" 197 | self._log.debug("shutdown initiated") 198 | 199 | self.flush() 200 | self.join() 201 | 202 | self._log.debug("shutdown completed") 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MyScale Telemetry 2 | 3 | The MyScale Telemetry is a powerful tool designed to enhance the observability of LLM applications by capturing trace data from LangChain-based applications and storing it in [MyScaleDB](https://github.com/myscale/MyScaleDB) or ClickHouse. This enables developers to diagnose issues, optimize performance, and gain deeper insights into their applications' behavior. 4 | 5 |

6 | Workflow of MyScale Telemetry 7 |

8 | 9 | ## Installation 10 | 11 | Install the MyScale Telemetry package using pip: 12 | 13 | ```bash 14 | pip install myscale-telemetry 15 | ``` 16 | 17 | ## Usage 18 | 19 | Here is an example of how to use the `MyScaleCallbackHandler` with LangChain: 20 | 21 | ```python 22 | import os 23 | from operator import itemgetter 24 | from myscale_telemetry.handler import MyScaleCallbackHandler 25 | from langchain_core.output_parsers import StrOutputParser 26 | from langchain_core.prompts import ChatPromptTemplate 27 | from langchain_openai import ChatOpenAI, OpenAIEmbeddings 28 | from langchain_community.vectorstores import MyScale, MyScaleSettings 29 | from langchain_core.runnables import RunnableConfig 30 | 31 | # set up the environment variables for OpenAI and MyScale Cloud/MyScaleDB 32 | os.environ["OPENAI_API_KEY"] = "YOUR_OPENAI_KEY" 33 | os.environ["MYSCALE_HOST"] = "YOUR_MYSCALE_HOST" 34 | os.environ["MYSCALE_PORT"] = "YOUR_MYSCALE_PORT" 35 | os.environ["MYSCALE_USERNAME"] = "YOUR_USERNAME" 36 | os.environ["MYSCALE_PASSWORD"] = "YOUR_MYSCALE_PASSWORD" 37 | 38 | # for MyScale cloud, you can set index_type="MSTG" for better performance compared to SCANN 39 | vectorstore = MyScale.from_texts(["harrison worked at kensho"], embedding=OpenAIEmbeddings(), config=MyScaleSettings(index_type="SCANN")) 40 | retriever = vectorstore.as_retriever() 41 | model = ChatOpenAI() 42 | template = """Answer the question based only on the following context: 43 | {context} 44 | 45 | Question: {question} 46 | 47 | """ 48 | prompt = ChatPromptTemplate.from_template(template) 49 | 50 | chain = ( 51 | { 52 | "context": itemgetter("question") | retriever, 53 | "question": itemgetter("question"), 54 | } 55 | | prompt 56 | | model 57 | | StrOutputParser() 58 | ) 59 | 60 | # integrate MyScaleCallbackHandler to capture trace data during the chain execution 61 | chain.invoke({"question": "where did harrison work"}, config=RunnableConfig( 62 | callbacks=[ 63 | MyScaleCallbackHandler() 64 | ] 65 | )) 66 | ``` 67 | 68 | In the default scenario, the callback handler generates a `trace_id` for a single Agent call. However, if you wish to integrate the Trace of the LLM call process with a higher-level caller, you can configure the `RunnableConfig` to pass in metadata with `trace_id` as the key during the call, as in the following example: 69 | 70 | ```python 71 | # trace_id obtained from the upper layer, such as request_id of http request 72 | trace_id = "http-request-id-xxx" 73 | chain.invoke({"question": "where did harrison work"}, config=RunnableConfig( 74 | callbacks=[ 75 | MyScaleCallbackHandler() 76 | ], 77 | metadata={"trace_id": trace_id} 78 | )) 79 | ``` 80 | 81 | ## Custom Parameters 82 | 83 | When invoking `MyScaleCallbackHandler()`, you can specify several parameters to customize its behavior. If not specified, the default values will be used. 84 | 85 | * `myscale_host`: MyScale database host (can also be set via `MYSCALE_HOST` environment variable) 86 | * `myscale_port`: MyScale database port (can also be set via `MYSCALE_PORT` environment variable) 87 | * `myscale_username`: MyScale database username (can also be set via `MYSCALE_USERNAME` environment variable) 88 | * `myscale_password`: MyScale database password (can also be set via `MYSCALE_PASSWORD` environment variable) 89 | * `threads`: Number of upload threads (default: 1) 90 | * `max_retries`: Maximum number of upload retries (default: 10) 91 | * `max_batch_size`: Maximum upload batch size (default: 1000) 92 | * `max_task_queue_size`: Maximum upload task queue size (default: 10000) 93 | * `upload_interval`: Upload interval in seconds (default: 0.5) 94 | * `database_name`: Name of the trace database (default: "otel") 95 | * `table_name`: Name of the trace table (default: "otel_traces") 96 | * `force_count_tokens`: Forces the calculation of LLM token usage, useful when OpenAI LLM streaming is enabled and token usage is not returned (default: False) 97 | * `encoding_name`: Name of the encoding used by tiktoken. This is only relevant if `force_count_tokens` is set to True (default: cl100k_base) 98 | 99 | ## Observability 100 | 101 | To display trace data collected through the MyScale Telemetry from the LLM Application runtime easily and clearly, we also provide a [Grafana Trace Dashboard](https://github.com/myscale/myscale-telemetry/blob/main/dashboard/grafana_myscale_trace_dashboard.json). 102 | The dashboard allows users to monitor the status of the LLM Application which is similar to LangSmith, making it easier to debug and improve its performance. 103 | 104 | ### Requirements 105 | 106 | * [Grafana](https://grafana.com/grafana) 107 | * [Official ClickHouse data source for Grafana](https://grafana.com/grafana/plugins/grafana-clickhouse-datasource/) 108 | * A compatible database instance. MyScale Telemetry supports [MyScaleDB](https://github.com/myscale/MyScaleDB), [MyScale Cloud](https://myscale.com/), and ClickHouse. 109 | 110 | ### Set up the Trace Dashboard 111 | 112 | Once you have Grafana, installed the ClickHouse data source plugin, and have a MyScale cluster with trace data collected through MyScale Telemetry, follow these steps to set up the MyScale Trace Dashboard in Grafana: 113 | 114 | 1. **Add a new ClickHouse Data Source in Grafana:** 115 | 116 | In the Grafana Data Source settings, add a new ClickHouse Data Source. Enter the Server Address, Server Port, Username, and Password to match those of your MyScale Cloud/MyScaleDB. 117 | ![Add a data source](https://github.com/myscale/myscale-telemetry/blob/main/assets/add_data_source.png?raw=True) 118 | ![Configure the data source](https://github.com/myscale/myscale-telemetry/blob/main/assets/config_data_source.png?raw=True) 119 | 120 | 2. **Import the MyScale Trace Dashboard:** 121 | 122 | Once the ClickHouse Data Source is added, you can import the [MyScale Trace Dashboard](https://github.com/myscale/myscale-telemetry/blob/main/dashboard/grafana_myscale_trace_dashboard.json?raw=True). 123 | 124 | ![Import the MyScale Trace Dashboard](https://github.com/myscale/myscale-telemetry/blob/main/assets/import_dashboard.png?raw=True) 125 | 126 | 3. **Configure the Dashboard:** 127 | 128 | After importing, select the MyScale Cluster (ClickHouse Data Source Name), the database name, table name, and TraceID of the trace you want to analyze. The dashboard will then display the Traces Table and the Trace Details Panel of the selected trace. 129 | 130 | ![Dashboard Example](https://github.com/myscale/myscale-telemetry/blob/main/assets/dashboard.png?raw=True) 131 | 132 | The MyScale Trace Dashboard provides comprehensive insights into the runtime behavior of your LLM applications, similar to LangSmith. It displays critical information that helps in debugging, optimizing, and understanding the performance of your applications. 133 | 134 | ## Roadmap 135 | 136 | * [ ] Support for more LLM frameworks 137 | * [ ] Support LLM tracing directly 138 | * [ ] Extend to end-to-end GenAI system observability 139 | 140 | ## Acknowledgment 141 | 142 | We give special thanks for these open-source projects: 143 | 144 | * [LangChain](https://github.com/langchain-ai/langchain): The most popular LLM framework integrated with MyScale Telemetry. 145 | * [OpenTelemetry](https://opentelemetry.io/): The schema of MyScale Telemetry is inspired by this widely used system telemetry toolset. 146 | * [MyScaleDB](https://github.com/myscale/MyScaleDB) and [ClickHouse](https://github.com/ClickHouse/ClickHouse): Data collected by MyScale Telemetry can be stored in either of these databases. 147 | -------------------------------------------------------------------------------- /dashboard/grafana_myscale_trace_dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [], 3 | "__elements": {}, 4 | "__requires": [ 5 | { 6 | "type": "grafana", 7 | "id": "grafana", 8 | "name": "Grafana", 9 | "version": "10.1.5" 10 | }, 11 | { 12 | "type": "datasource", 13 | "id": "grafana-clickhouse-datasource", 14 | "name": "ClickHouse", 15 | "version": "4.0.2" 16 | }, 17 | { 18 | "type": "panel", 19 | "id": "table", 20 | "name": "Table", 21 | "version": "" 22 | }, 23 | { 24 | "type": "panel", 25 | "id": "traces", 26 | "name": "Traces", 27 | "version": "" 28 | } 29 | ], 30 | "annotations": { 31 | "list": [ 32 | { 33 | "builtIn": 1, 34 | "datasource": { 35 | "type": "grafana", 36 | "uid": "-- Grafana --" 37 | }, 38 | "enable": true, 39 | "hide": true, 40 | "iconColor": "rgba(0, 211, 255, 1)", 41 | "name": "Annotations & Alerts", 42 | "type": "dashboard" 43 | } 44 | ] 45 | }, 46 | "description": "", 47 | "editable": true, 48 | "fiscalYearStartMonth": 0, 49 | "graphTooltip": 0, 50 | "id": null, 51 | "links": [], 52 | "liveNow": false, 53 | "panels": [ 54 | { 55 | "datasource": { 56 | "type": "grafana-clickhouse-datasource", 57 | "uid": "${datasource}" 58 | }, 59 | "fieldConfig": { 60 | "defaults": { 61 | "custom": { 62 | "align": "auto", 63 | "cellOptions": { 64 | "type": "auto" 65 | }, 66 | "inspect": false 67 | }, 68 | "mappings": [], 69 | "thresholds": { 70 | "mode": "absolute", 71 | "steps": [ 72 | { 73 | "color": "green", 74 | "value": null 75 | }, 76 | { 77 | "color": "red", 78 | "value": 80 79 | } 80 | ] 81 | } 82 | }, 83 | "overrides": [] 84 | }, 85 | "gridPos": { 86 | "h": 6, 87 | "w": 24, 88 | "x": 0, 89 | "y": 0 90 | }, 91 | "id": 1, 92 | "options": { 93 | "cellHeight": "sm", 94 | "footer": { 95 | "countRows": false, 96 | "fields": "", 97 | "reducer": [ 98 | "sum" 99 | ], 100 | "show": false 101 | }, 102 | "showHeader": true, 103 | "sortBy": [] 104 | }, 105 | "pluginVersion": "10.1.5", 106 | "targets": [ 107 | { 108 | "builderOptions": { 109 | "columns": [ 110 | { 111 | "hint": "trace_id", 112 | "name": "TraceId", 113 | "type": "String" 114 | }, 115 | { 116 | "hint": "trace_span_id", 117 | "name": "SpanId", 118 | "type": "String" 119 | }, 120 | { 121 | "hint": "trace_parent_span_id", 122 | "name": "ParentSpanId", 123 | "type": "String" 124 | }, 125 | { 126 | "hint": "trace_service_name", 127 | "name": "ServiceName", 128 | "type": "LowCardinality(String)" 129 | }, 130 | { 131 | "hint": "trace_operation_name", 132 | "name": "SpanName", 133 | "type": "LowCardinality(String)" 134 | }, 135 | { 136 | "hint": "time", 137 | "name": "StartTime", 138 | "type": "DateTime64(9)" 139 | }, 140 | { 141 | "hint": "trace_duration_time", 142 | "name": "Duration", 143 | "type": "Int64" 144 | }, 145 | { 146 | "hint": "trace_tags", 147 | "name": "SpanAttributes", 148 | "type": "Map(LowCardinality(String), String)" 149 | }, 150 | { 151 | "hint": "trace_service_tags", 152 | "name": "ResourceAttributes", 153 | "type": "Map(LowCardinality(String), String)" 154 | } 155 | ], 156 | "database": "$Database", 157 | "filters": [ 158 | { 159 | "condition": "AND", 160 | "filterType": "custom", 161 | "hint": "time", 162 | "key": "", 163 | "operator": "IS ANYTHING", 164 | "type": "datetime" 165 | }, 166 | { 167 | "condition": "AND", 168 | "filterType": "custom", 169 | "hint": "trace_parent_span_id", 170 | "key": "", 171 | "operator": "IS EMPTY", 172 | "type": "string", 173 | "value": "" 174 | }, 175 | { 176 | "condition": "AND", 177 | "filterType": "custom", 178 | "hint": "trace_duration_time", 179 | "key": "", 180 | "operator": ">", 181 | "type": "UInt64", 182 | "value": 0 183 | }, 184 | { 185 | "condition": "AND", 186 | "filterType": "custom", 187 | "hint": "trace_service_name", 188 | "key": "", 189 | "operator": "IS ANYTHING", 190 | "type": "string", 191 | "value": "" 192 | } 193 | ], 194 | "limit": 1000, 195 | "meta": { 196 | "isTraceIdMode": false, 197 | "otelVersion": "latest", 198 | "traceDurationUnit": "microseconds", 199 | "traceId": "" 200 | }, 201 | "mode": "list", 202 | "orderBy": [ 203 | { 204 | "default": true, 205 | "dir": "DESC", 206 | "hint": "time", 207 | "name": "" 208 | }, 209 | { 210 | "default": true, 211 | "dir": "DESC", 212 | "hint": "trace_duration_time", 213 | "name": "" 214 | } 215 | ], 216 | "queryType": "traces", 217 | "table": "$Table" 218 | }, 219 | "datasource": { 220 | "type": "grafana-clickhouse-datasource", 221 | "uid": "${datasource}" 222 | }, 223 | "editorType": "builder", 224 | "format": 1, 225 | "pluginVersion": "4.0.2", 226 | "rawSql": "SELECT \"TraceId\" as traceID, \"ServiceName\" as serviceName, \"SpanName\" as operationName, \"StartTime\" as startTime, intDivOrZero(\"Duration\", 1000) as \"duration(ms)\" FROM \"$Database\".\"$Table\" WHERE ( ParentSpanId = '' ) AND ( Duration > 0 ) ORDER BY StartTime DESC, Duration DESC LIMIT 1000", 227 | "refId": "A" 228 | } 229 | ], 230 | "title": "Traces", 231 | "type": "table" 232 | }, 233 | { 234 | "datasource": { 235 | "type": "grafana-clickhouse-datasource", 236 | "uid": "${datasource}" 237 | }, 238 | "gridPos": { 239 | "h": 15, 240 | "w": 24, 241 | "x": 0, 242 | "y": 6 243 | }, 244 | "id": 2, 245 | "targets": [ 246 | { 247 | "builderOptions": { 248 | "columns": [ 249 | { 250 | "hint": "trace_id", 251 | "name": "TraceId", 252 | "type": "String" 253 | }, 254 | { 255 | "hint": "trace_span_id", 256 | "name": "SpanId", 257 | "type": "String" 258 | }, 259 | { 260 | "hint": "trace_parent_span_id", 261 | "name": "ParentSpanId", 262 | "type": "String" 263 | }, 264 | { 265 | "hint": "trace_service_name", 266 | "name": "ServiceName", 267 | "type": "LowCardinality(String)" 268 | }, 269 | { 270 | "hint": "trace_operation_name", 271 | "name": "SpanName", 272 | "type": "LowCardinality(String)" 273 | }, 274 | { 275 | "hint": "time", 276 | "name": "StartTime", 277 | "type": "DateTime64(9)" 278 | }, 279 | { 280 | "hint": "trace_duration_time", 281 | "name": "Duration", 282 | "type": "Int64" 283 | }, 284 | { 285 | "hint": "trace_tags", 286 | "name": "SpanAttributes", 287 | "type": "Map(LowCardinality(String), String)" 288 | }, 289 | { 290 | "hint": "trace_service_tags", 291 | "name": "ResourceAttributes", 292 | "type": "Map(LowCardinality(String), String)" 293 | } 294 | ], 295 | "database": "$Database", 296 | "filters": [], 297 | "limit": 1000, 298 | "meta": { 299 | "isTraceIdMode": true, 300 | "minimized": false, 301 | "otelVersion": "latest", 302 | "traceDurationUnit": "microseconds", 303 | "traceId": "$TraceID" 304 | }, 305 | "mode": "list", 306 | "orderBy": [], 307 | "queryType": "traces", 308 | "table": "$Table" 309 | }, 310 | "datasource": { 311 | "type": "grafana-clickhouse-datasource", 312 | "uid": "${datasource}" 313 | }, 314 | "editorType": "builder", 315 | "format": 3, 316 | "pluginVersion": "4.0.2", 317 | "rawSql": "SELECT \"TraceId\" as traceID, \"SpanId\" as spanID, \"ParentSpanId\" as parentSpanID, \"ServiceName\" as serviceName, \"SpanName\" as operationName, \"StartTime\" as startTime, intDivOrZero(\"Duration\", 1000) as duration, arrayMap(key -> map('key', key, 'value',\"SpanAttributes\"[key]), mapKeys(\"SpanAttributes\")) as tags, arrayMap(key -> map('key', key, 'value',\"ResourceAttributes\"[key]), mapKeys(\"ResourceAttributes\")) as serviceTags FROM \"$Database\".\"$Table\" WHERE traceID = '$TraceID' LIMIT 1000", 318 | "refId": "Trace ID" 319 | } 320 | ], 321 | "title": "Trace Details", 322 | "type": "traces" 323 | } 324 | ], 325 | "refresh": "", 326 | "schemaVersion": 38, 327 | "style": "dark", 328 | "tags": [ 329 | "MyScale", 330 | "Trace" 331 | ], 332 | "templating": { 333 | "list": [ 334 | { 335 | "current": {}, 336 | "hide": 0, 337 | "includeAll": false, 338 | "label": "MyScale Cluster", 339 | "multi": false, 340 | "name": "datasource", 341 | "options": [], 342 | "query": "grafana-clickhouse-datasource", 343 | "queryValue": "", 344 | "refresh": 1, 345 | "regex": "", 346 | "skipUrlSync": false, 347 | "type": "datasource" 348 | }, 349 | { 350 | "current": {}, 351 | "datasource": { 352 | "type": "grafana-clickhouse-datasource", 353 | "uid": "${datasource}" 354 | }, 355 | "definition": "", 356 | "hide": 0, 357 | "includeAll": false, 358 | "multi": false, 359 | "name": "Database", 360 | "options": [], 361 | "query": "SHOW DATABASES", 362 | "refresh": 1, 363 | "regex": "", 364 | "skipUrlSync": false, 365 | "sort": 0, 366 | "type": "query" 367 | }, 368 | { 369 | "current": {}, 370 | "datasource": { 371 | "type": "grafana-clickhouse-datasource", 372 | "uid": "${datasource}" 373 | }, 374 | "definition": "", 375 | "hide": 0, 376 | "includeAll": false, 377 | "multi": false, 378 | "name": "Table", 379 | "options": [], 380 | "query": "SHOW TABLES FROM $Database", 381 | "refresh": 2, 382 | "regex": "/^(?!system\\.).*/", 383 | "skipUrlSync": false, 384 | "sort": 0, 385 | "type": "query" 386 | }, 387 | { 388 | "current": {}, 389 | "datasource": { 390 | "type": "grafana-clickhouse-datasource", 391 | "uid": "${datasource}" 392 | }, 393 | "definition": "", 394 | "hide": 0, 395 | "includeAll": false, 396 | "multi": false, 397 | "name": "TraceID", 398 | "options": [], 399 | "query": "SELECT DISTINCT TraceId FROM $Database.$Table", 400 | "refresh": 1, 401 | "regex": "", 402 | "skipUrlSync": false, 403 | "sort": 0, 404 | "type": "query" 405 | } 406 | ] 407 | }, 408 | "time": { 409 | "from": "now-24h", 410 | "to": "now" 411 | }, 412 | "timepicker": {}, 413 | "timezone": "", 414 | "title": "MyScale Trace Dashboard", 415 | "version": 1, 416 | "weekStart": "" 417 | } -------------------------------------------------------------------------------- /myscale_telemetry/handler.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import json 4 | import datetime 5 | from typing import Any, Dict, List, Union, Optional, Sequence, cast 6 | from uuid import UUID 7 | import tiktoken 8 | from langchain.callbacks.base import BaseCallbackHandler 9 | from langchain.schema.document import Document 10 | from langchain_core.messages import ( 11 | AIMessage, 12 | BaseMessage, 13 | ChatMessage, 14 | HumanMessage, 15 | SystemMessage, 16 | ToolMessage, 17 | FunctionMessage, 18 | ) 19 | from langchain_core.outputs import LLMResult, ChatGeneration 20 | from langchain_community.callbacks.utils import flatten_dict 21 | from .task_manager import TaskManager 22 | 23 | STATUS_SUCCESS = "STATUS_CODE_SUCCESS" 24 | STATUS_ERROR = "STATUS_CODE_ERROR" 25 | 26 | 27 | def get_timestamp() -> datetime.datetime: 28 | """Return the current UTC timestamp.""" 29 | return datetime.datetime.now(datetime.timezone.utc) 30 | 31 | 32 | def _extract_prompt_templates(serialized) -> Dict[str, str]: 33 | """Extract prompt templates from serialized data.""" 34 | prompts_dict = {} 35 | flat_dict = flatten_dict(serialized) 36 | 37 | for i, message in enumerate(flat_dict.get("kwargs_messages", [])): 38 | prompt_template = ( 39 | message.get("kwargs", {}) 40 | .get("prompt", {}) 41 | .get("kwargs", {}) 42 | .get("template") 43 | ) 44 | if isinstance(prompt_template, str): 45 | prompts_dict[f"prompts.{i}.template"] = prompt_template 46 | 47 | return prompts_dict 48 | 49 | 50 | def _convert_message_to_dict(message: BaseMessage, prefix_key: str) -> Dict[str, str]: 51 | """Convert a message to a dictionary with a specified prefix key.""" 52 | role_mapping = { 53 | HumanMessage: "user", 54 | AIMessage: "assistant", 55 | SystemMessage: "system", 56 | ToolMessage: "tool", 57 | FunctionMessage: "function", 58 | } 59 | 60 | role = None 61 | 62 | for msg_type, mapped_role in role_mapping.items(): 63 | if isinstance(message, msg_type): 64 | role = mapped_role 65 | break 66 | 67 | if role is None and isinstance(message, ChatMessage): 68 | role = message.role 69 | 70 | if role is not None: 71 | return { 72 | f"{prefix_key}role": role, 73 | f"{prefix_key}content": cast(str, message.content), 74 | } 75 | 76 | return {} 77 | 78 | 79 | def _extract_resource_attributes( 80 | metadata: Dict[str, Any], serialized: Dict[str, Any] 81 | ) -> Dict[str, str]: 82 | """Extract resource attributes from serialized data.""" 83 | resource_attributes: Dict[str, str] = {} 84 | 85 | flat_dict = flatten_dict(serialized) if serialized else {} 86 | flat_dict.update(metadata) 87 | for resource_key, resource_val in flat_dict.items(): 88 | resource_attributes.update({resource_key: _serialize_value(resource_val)}) 89 | 90 | return resource_attributes 91 | 92 | 93 | def _serialize_value(value: Any) -> str: 94 | """Serialize the given value to a string.""" 95 | if isinstance(value, str): 96 | return value 97 | try: 98 | return json.dumps(value, ensure_ascii=False) 99 | except (TypeError, ValueError): 100 | try: 101 | return str(value) 102 | except Exception: 103 | return f"" 104 | 105 | 106 | def _extract_span_attributes(data: Any, **kwargs: Any) -> Dict[str, str]: 107 | """Extract span attributes from the given data, converting serializable objects to strings.""" 108 | 109 | span_attributes: Dict[str, str] = {} 110 | 111 | if isinstance(data, dict): 112 | for attributes_key, attributes_val in data.items(): 113 | if isinstance(attributes_key, str): 114 | span_attributes[attributes_key] = _serialize_value(attributes_val) 115 | elif isinstance(data, list): 116 | for i, item in enumerate(data): 117 | if isinstance(item, BaseMessage): 118 | span_attributes.update(_convert_message_to_dict(item, f"prompts.{i}.")) 119 | elif isinstance(item, Document): 120 | span_attributes[f"documents.{i}.content"] = item.page_content 121 | else: 122 | span_attributes[f"items.{i}.content"] = _serialize_value(item) 123 | 124 | params = kwargs.get("invocation_params", {}) 125 | if params: 126 | model = ( 127 | params.get("model") or params.get("model_name") or params.get("model_id") 128 | ) 129 | chat_type = params.get("_type") 130 | temperature = str(params.get("temperature")) 131 | span_attributes.update( 132 | {"model": model, "chat_type": chat_type, "temperature": temperature} 133 | ) 134 | 135 | return span_attributes 136 | 137 | 138 | def _get_langchain_run_name(serialized: Optional[Dict[str, Any]], **kwargs: Any) -> str: 139 | """Retrieve the name of a serialized LangChain runnable.""" 140 | if serialized: 141 | name = serialized.get("name", serialized.get("id", ["Unknown"])[-1]) 142 | else: 143 | if "name" in kwargs and kwargs["name"]: 144 | name = kwargs["name"] 145 | else: 146 | name = "Unknown" 147 | 148 | return name 149 | 150 | 151 | def num_tokens_from_string(string: str, encoding_name: str) -> int: 152 | """Returns the number of tokens in a text string.""" 153 | encoding = tiktoken.get_encoding(encoding_name) 154 | num_tokens = len(encoding.encode(string)) 155 | return num_tokens 156 | 157 | 158 | class MyScaleCallbackHandler(BaseCallbackHandler): 159 | """Callback Handler for MyScale. 160 | 161 | Parameters: 162 | myscale_host (Optional[str]): The hostname of the MyScale database. 163 | myscale_port (Optional[int]): The port of the MyScale database. 164 | myscale_username (Optional[str]): The username for the MyScale database. 165 | myscale_password (Optional[str]): The password for the MyScale database. 166 | threads (int): The number of threads for uploading data to MyScale. 167 | max_retries (int): The maximum number of retries for uploading data to MyScale. 168 | max_batch_size (int): The maximum batch size for uploading data to MyScale. 169 | max_task_queue_size (int): The maximum size of the task queue. 170 | upload_interval (float): The interval between uploads in seconds. 171 | database_name (str): The name of the database to use. 172 | table_name (str): The name of the table to use. 173 | force_count_tokens (bool): Forces the calculation of LLM token usage, 174 | useful when OpenAI LLM streaming is enabled 175 | and token usage is not returned. 176 | encoding_name (str): The name of the encoding used by tiktoken. 177 | This is only relevant if `force_count_tokens` is set to True. 178 | 179 | This handler utilizes callback methods to extract various elements such as 180 | questions, retrieved documents, prompts, and messages from each callback 181 | function, and subsequently uploads this data to the MyScale vector database 182 | for monitoring and evaluating the performance of the LLM application. 183 | """ 184 | 185 | def __init__( 186 | self, 187 | myscale_host: Optional[str] = None, 188 | myscale_port: Optional[int] = None, 189 | myscale_username: Optional[str] = None, 190 | myscale_password: Optional[str] = None, 191 | threads: int = 1, 192 | max_retries: int = 10, 193 | max_batch_size: int = 1000, 194 | max_task_queue_size: int = 10000, 195 | upload_interval: float = 0.5, 196 | database_name: str = "otel", 197 | table_name: str = "otel_traces", 198 | force_count_tokens: bool = False, 199 | encoding_name: str = "cl100k_base", 200 | ) -> None: 201 | """Set up the MyScale client and the TaskManager, 202 | which is responsible for uploading data to the MyScale vector database.""" 203 | try: 204 | from clickhouse_connect import get_client 205 | except ImportError as exc: 206 | raise ImportError( 207 | "Could not import clickhouse connect python package. " 208 | "Please install it with `pip install clickhouse-connect`." 209 | ) from exc 210 | 211 | self._log = logging.getLogger(__name__) 212 | self.myscale_host = myscale_host or os.getenv("MYSCALE_HOST") 213 | self.myscale_port = myscale_port or int(os.getenv("MYSCALE_PORT", "443")) 214 | self.myscale_username = myscale_username or os.getenv("MYSCALE_USERNAME") 215 | self.myscale_password = myscale_password or os.getenv("MYSCALE_PASSWORD") 216 | self.myscale_client = get_client( 217 | host=self.myscale_host, 218 | port=self.myscale_port, 219 | username=self.myscale_username, 220 | password=self.myscale_password, 221 | ) 222 | 223 | self.force_count_tokens = force_count_tokens 224 | self.encoding_name = encoding_name 225 | 226 | self._task_manager = TaskManager( 227 | client=self.myscale_client, 228 | threads=threads, 229 | max_retries=max_retries, 230 | max_batch_size=max_batch_size, 231 | max_task_queue_size=max_task_queue_size, 232 | upload_interval=upload_interval, 233 | database_name=database_name, 234 | table_name=table_name, 235 | ) 236 | 237 | def on_chain_start( 238 | self, 239 | serialized: Optional[Dict[str, Any]], 240 | inputs: Dict[str, Any], 241 | *, 242 | run_id: UUID, 243 | parent_run_id: Optional[UUID] = None, 244 | tags: Optional[List[str]] = None, 245 | metadata: Optional[Dict[str, Any]] = None, 246 | **kwargs: Any, 247 | ) -> Any: 248 | """Run when chain starts running.""" 249 | self._log.debug( 250 | "on chain start run_id: %s parent_run_id: %s", run_id, parent_run_id 251 | ) 252 | try: 253 | if metadata and metadata.get("trace_id"): 254 | trace_id = metadata["trace_id"] 255 | else: 256 | if parent_run_id is None: 257 | trace_id = self._task_manager.create_trace_id() 258 | else: 259 | trace_id = self._task_manager.get_trace_id() 260 | 261 | name = _get_langchain_run_name(serialized, **kwargs) 262 | 263 | span_attributes = {} 264 | if isinstance(inputs, str): 265 | span_attributes["input"] = inputs 266 | else: 267 | span_attributes.update(_extract_span_attributes(inputs, **kwargs)) 268 | 269 | if name == "ChatPromptTemplate" and serialized: 270 | span_attributes.update(_extract_prompt_templates(serialized)) 271 | 272 | self._task_manager.create_span( 273 | trace_id=trace_id, 274 | span_id=run_id, 275 | parent_span_id=parent_run_id, 276 | start_time=get_timestamp(), 277 | name=name, 278 | kind="chain", 279 | span_attributes=span_attributes, 280 | resource_attributes=_extract_resource_attributes(metadata, serialized), 281 | ) 282 | 283 | except Exception as e: 284 | self._log.exception("An error occurred in on_chain_start: %s", e) 285 | 286 | def on_chain_end( 287 | self, 288 | outputs: Dict[str, Any], 289 | *, 290 | run_id: UUID, 291 | parent_run_id: Optional[UUID] = None, 292 | **kwargs: Any, 293 | ) -> Any: 294 | """Run when chain ends running.""" 295 | self._log.debug( 296 | "on chain end run_id: %s parent_run_id: %s", run_id, parent_run_id 297 | ) 298 | try: 299 | span_attributes = {} 300 | if isinstance(outputs, str): 301 | span_attributes["output"] = outputs 302 | else: 303 | span_attributes.update(_extract_span_attributes(outputs, **kwargs)) 304 | 305 | self._task_manager.end_span( 306 | span_id=run_id, 307 | end_time=get_timestamp(), 308 | span_attributes=span_attributes, 309 | status_code=STATUS_SUCCESS, 310 | status_message="", 311 | ) 312 | 313 | except Exception as e: 314 | self._log.exception("An error occurred in on_chain_end: %s", e) 315 | 316 | def on_llm_start( 317 | self, 318 | serialized: Optional[Dict[str, Any]], 319 | prompts: List[str], 320 | *, 321 | run_id: UUID, 322 | parent_run_id: Union[UUID, None] = None, 323 | tags: Union[List[str], None] = None, 324 | metadata: Union[Dict[str, Any], None] = None, 325 | **kwargs: Any, 326 | ) -> Any: 327 | """Run when LLM starts running.""" 328 | self._log.debug( 329 | "on llm start run_id: %s parent_run_id: %s", run_id, parent_run_id 330 | ) 331 | try: 332 | if metadata and metadata.get("trace_id"): 333 | trace_id = metadata["trace_id"] 334 | else: 335 | trace_id = self._task_manager.get_trace_id() 336 | 337 | span_attributes = _extract_span_attributes(prompts, **kwargs) 338 | if self.force_count_tokens: 339 | prompt_tokens = 0 340 | for i in range(len(prompts)): 341 | content_key = "prompts." + str(i) + ".content" 342 | if content_key in span_attributes: 343 | prompt_tokens += num_tokens_from_string( 344 | span_attributes[content_key], self.encoding_name 345 | ) 346 | 347 | span_attributes["prompt_tokens"] = str(prompt_tokens) 348 | 349 | self._task_manager.create_span( 350 | trace_id=trace_id, 351 | span_id=run_id, 352 | parent_span_id=parent_run_id, 353 | start_time=get_timestamp(), 354 | name=_get_langchain_run_name(serialized, **kwargs), 355 | kind="llm", 356 | span_attributes=span_attributes, 357 | resource_attributes=_extract_resource_attributes(metadata, serialized), 358 | ) 359 | except Exception as e: 360 | self._log.exception("An error occurred in on_llm_start: %s", e) 361 | 362 | def on_llm_end( 363 | self, 364 | response: LLMResult, 365 | *, 366 | run_id: UUID, 367 | parent_run_id: Optional[UUID] = None, 368 | **kwargs: Any, 369 | ) -> Any: 370 | """Run when LLM ends running.""" 371 | self._log.debug( 372 | "on llm end run_id: %s parent_run_id: %s", run_id, parent_run_id 373 | ) 374 | try: 375 | span_attributes: Dict[str, str] = {} 376 | for i, generation in enumerate(response.generations): 377 | generation = generation[0] 378 | prefix_key = "completions." + str(i) + "." 379 | if isinstance(generation, ChatGeneration): 380 | span_attributes.update( 381 | _convert_message_to_dict(generation.message, prefix_key) 382 | ) 383 | else: 384 | span_attributes[f"{prefix_key}content"] = generation.text 385 | 386 | if self.force_count_tokens: 387 | completion_tokens = 0 388 | for i in range(len(response.generations)): 389 | content_key = "completions." + str(i) + ".content" 390 | if content_key in span_attributes: 391 | completion_tokens += num_tokens_from_string( 392 | span_attributes[content_key], self.encoding_name 393 | ) 394 | 395 | span_attributes["completion_tokens"] = str(completion_tokens) 396 | else: 397 | if response.llm_output is not None and isinstance( 398 | response.llm_output, Dict 399 | ): 400 | token_usage = response.llm_output["token_usage"] 401 | if token_usage is not None: 402 | span_attributes["prompt_tokens"] = str( 403 | token_usage["prompt_tokens"] 404 | ) 405 | span_attributes["total_tokens"] = str( 406 | token_usage["total_tokens"] 407 | ) 408 | span_attributes["completion_tokens"] = str( 409 | token_usage["completion_tokens"] 410 | ) 411 | 412 | self._task_manager.end_span( 413 | span_id=run_id, 414 | end_time=get_timestamp(), 415 | span_attributes=span_attributes, 416 | status_code=STATUS_SUCCESS, 417 | status_message="", 418 | force_count_tokens=self.force_count_tokens, 419 | ) 420 | 421 | except Exception as e: 422 | self._log.exception("An error occurred in on_llm_end: %s", e) 423 | 424 | def on_chat_model_start( 425 | self, 426 | serialized: Dict[str, Any], 427 | messages: List[List[BaseMessage]], 428 | *, 429 | run_id: UUID, 430 | parent_run_id: Optional[UUID] = None, 431 | tags: Optional[List[str]] = None, 432 | metadata: Optional[Dict[str, Any]] = None, 433 | **kwargs: Any, 434 | ) -> Any: 435 | """Run when a chat model starts running.""" 436 | self._log.debug( 437 | "on chat model start run_id: %s parent_run_id: %s", run_id, parent_run_id 438 | ) 439 | try: 440 | if metadata and metadata.get("trace_id"): 441 | trace_id = metadata["trace_id"] 442 | else: 443 | trace_id = self._task_manager.get_trace_id() 444 | 445 | flattened_messages = [item for sublist in messages for item in sublist] 446 | span_attributes = _extract_span_attributes(flattened_messages, **kwargs) 447 | if self.force_count_tokens: 448 | prompt_tokens = 0 449 | for i in range(len(flattened_messages)): 450 | content_key = "prompts." + str(i) + ".content" 451 | if content_key in span_attributes: 452 | prompt_tokens += num_tokens_from_string( 453 | span_attributes[content_key], self.encoding_name 454 | ) 455 | 456 | span_attributes["prompt_tokens"] = str(prompt_tokens) 457 | 458 | self._task_manager.create_span( 459 | trace_id=trace_id, 460 | span_id=run_id, 461 | parent_span_id=parent_run_id, 462 | start_time=get_timestamp(), 463 | name=_get_langchain_run_name(serialized, **kwargs), 464 | kind="llm", 465 | span_attributes=span_attributes, 466 | resource_attributes=_extract_resource_attributes(metadata, serialized), 467 | ) 468 | except Exception as e: 469 | self._log.exception("An error occurred in on_chat_model_start: %s", e) 470 | 471 | def on_retriever_start( 472 | self, 473 | serialized: Optional[Dict[str, Any]], 474 | query: str, 475 | *, 476 | run_id: UUID, 477 | parent_run_id: Optional[UUID] = None, 478 | tags: Optional[List[str]] = None, 479 | metadata: Optional[Dict[str, Any]] = None, 480 | **kwargs: Any, 481 | ) -> Any: 482 | """Run when Retriever starts running.""" 483 | self._log.debug( 484 | "on retriever start run_id: %s parent_run_id: %s", run_id, parent_run_id 485 | ) 486 | try: 487 | if metadata and metadata.get("trace_id"): 488 | trace_id = metadata["trace_id"] 489 | else: 490 | trace_id = self._task_manager.get_trace_id() 491 | 492 | self._task_manager.create_span( 493 | trace_id=trace_id, 494 | span_id=run_id, 495 | parent_span_id=parent_run_id, 496 | start_time=get_timestamp(), 497 | name=_get_langchain_run_name(serialized, **kwargs), 498 | kind="retriever", 499 | span_attributes={"query": query}, 500 | resource_attributes=_extract_resource_attributes(metadata, serialized), 501 | ) 502 | 503 | except Exception as e: 504 | self._log.exception("An error occurred in on_retriever_start: %s", e) 505 | 506 | def on_retriever_end( 507 | self, 508 | documents: Sequence[Document], 509 | *, 510 | run_id: UUID, 511 | parent_run_id: Optional[UUID] = None, 512 | **kwargs: Any, 513 | ) -> Any: 514 | """Run when Retriever ends running.""" 515 | self._log.debug( 516 | "on retriever end run_id: %s parent_run_id: %s", run_id, parent_run_id 517 | ) 518 | try: 519 | span_attributes: Dict[str, str] = {} 520 | for i, document in enumerate(documents): 521 | span_attributes[f"documents.{i}.content"] = document.page_content 522 | 523 | self._task_manager.end_span( 524 | span_id=run_id, 525 | end_time=get_timestamp(), 526 | span_attributes=span_attributes, 527 | status_code=STATUS_SUCCESS, 528 | status_message="", 529 | ) 530 | 531 | except Exception as e: 532 | self._log.exception("An error occurred in on_chain_end: %s", e) 533 | 534 | def on_tool_start( 535 | self, 536 | serialized: Dict[str, Any], 537 | input_str: str, 538 | *, 539 | run_id: UUID, 540 | parent_run_id: Optional[UUID] = None, 541 | tags: Optional[List[str]] = None, 542 | metadata: Optional[Dict[str, Any]] = None, 543 | inputs: Optional[Dict[str, Any]] = None, 544 | **kwargs: Any, 545 | ) -> Any: 546 | """Run when tool starts running.""" 547 | self._log.debug( 548 | "on tool start run_id: %s parent_run_id: %s", run_id, parent_run_id 549 | ) 550 | try: 551 | if metadata and metadata.get("trace_id"): 552 | trace_id = metadata["trace_id"] 553 | else: 554 | trace_id = self._task_manager.get_trace_id() 555 | 556 | span_attributes = {} 557 | if isinstance(input_str, str): 558 | span_attributes["input"] = input_str 559 | else: 560 | span_attributes.update(_extract_span_attributes(input_str, **kwargs)) 561 | 562 | self._task_manager.create_span( 563 | trace_id=trace_id, 564 | span_id=run_id, 565 | parent_span_id=parent_run_id, 566 | start_time=get_timestamp(), 567 | name=_get_langchain_run_name(serialized, **kwargs), 568 | kind="tool", 569 | span_attributes=span_attributes, 570 | resource_attributes=_extract_resource_attributes(metadata, serialized), 571 | ) 572 | 573 | except Exception as e: 574 | self._log.exception("An error occurred in on_tool_start: %s", e) 575 | 576 | def on_tool_end( 577 | self, 578 | output: str, 579 | *, 580 | run_id: UUID, 581 | parent_run_id: Optional[UUID] = None, 582 | **kwargs: Any, 583 | ) -> Any: 584 | """Run when tool ends running.""" 585 | self._log.debug( 586 | "on tool end run_id: %s parent_run_id: %s", run_id, parent_run_id 587 | ) 588 | try: 589 | span_attributes = {} 590 | if isinstance(output, str): 591 | span_attributes["output"] = output 592 | else: 593 | span_attributes.update(_extract_span_attributes(output, **kwargs)) 594 | self._task_manager.end_span( 595 | span_id=run_id, 596 | end_time=get_timestamp(), 597 | span_attributes=span_attributes, 598 | status_code=STATUS_SUCCESS, 599 | status_message="", 600 | ) 601 | except Exception as e: 602 | self._log.exception("An error occurred in on_tool_end: %s", e) 603 | 604 | def on_tool_error( 605 | self, 606 | error: BaseException, 607 | *, 608 | run_id: UUID, 609 | parent_run_id: Optional[UUID] = None, 610 | **kwargs: Any, 611 | ) -> Any: 612 | """Run when tool errors.""" 613 | self._log.debug( 614 | "on tool error run_id: %s parent_run_id: %s", run_id, parent_run_id 615 | ) 616 | try: 617 | self._task_manager.end_span( 618 | span_id=run_id, 619 | end_time=get_timestamp(), 620 | span_attributes={}, 621 | status_code=STATUS_ERROR, 622 | status_message=f"An error occurred in a tool: {str(error)}", 623 | ) 624 | except Exception as e: 625 | self._log.exception("An error occurred in on_tool_error: %s", e) 626 | 627 | def on_chain_error( 628 | self, 629 | error: BaseException, 630 | *, 631 | run_id: UUID, 632 | parent_run_id: Optional[UUID] = None, 633 | **kwargs: Any, 634 | ) -> Any: 635 | """Run when chain errors.""" 636 | self._log.debug( 637 | "on chain error run_id: %s parent_run_id: %s", run_id, parent_run_id 638 | ) 639 | try: 640 | self._task_manager.end_span( 641 | span_id=run_id, 642 | end_time=get_timestamp(), 643 | span_attributes={}, 644 | status_code=STATUS_ERROR, 645 | status_message=f"An error occurred in a chain: {str(error)}", 646 | ) 647 | except Exception as e: 648 | self._log.exception("An error occurred in on_chain_error: %s", e) 649 | 650 | def on_retriever_error( 651 | self, 652 | error: BaseException, 653 | *, 654 | run_id: UUID, 655 | parent_run_id: Optional[UUID] = None, 656 | **kwargs: Any, 657 | ) -> Any: 658 | """Run when Retriever errors.""" 659 | self._log.debug( 660 | "on retriever error run_id: %s parent_run_id: %s", run_id, parent_run_id 661 | ) 662 | try: 663 | self._task_manager.end_span( 664 | span_id=run_id, 665 | end_time=get_timestamp(), 666 | span_attributes={}, 667 | status_code=STATUS_ERROR, 668 | status_message=f"An error occurred in a retriever: {str(error)}", 669 | ) 670 | except Exception as e: 671 | self._log.exception("An error occurred in on_retriever_error: %s", e) 672 | 673 | def on_llm_error( 674 | self, 675 | error: BaseException, 676 | *, 677 | run_id: UUID, 678 | parent_run_id: Optional[UUID] = None, 679 | **kwargs: Any, 680 | ) -> Any: 681 | """Run when LLM errors.""" 682 | self._log.debug( 683 | "on llm error run_id: %s parent_run_id: %s", run_id, parent_run_id 684 | ) 685 | try: 686 | self._task_manager.end_span( 687 | span_id=run_id, 688 | end_time=get_timestamp(), 689 | span_attributes={}, 690 | status_code=STATUS_ERROR, 691 | status_message=f"An error occurred in a llm: {str(error)}", 692 | ) 693 | except Exception as e: 694 | self._log.exception("An error occurred in on_llm_error: %s", e) 695 | -------------------------------------------------------------------------------- /assets/workflow.excalidraw: -------------------------------------------------------------------------------- 1 | { 2 | "type": "excalidraw", 3 | "version": 2, 4 | "source": "https://marketplace.visualstudio.com/items?itemName=pomdtr.excalidraw-editor", 5 | "elements": [ 6 | { 7 | "type": "rectangle", 8 | "version": 652, 9 | "versionNonce": 1923476312, 10 | "isDeleted": false, 11 | "id": "m55XkYxvo2oab2q7saDF9", 12 | "fillStyle": "hachure", 13 | "strokeWidth": 2, 14 | "strokeStyle": "dashed", 15 | "roughness": 0, 16 | "opacity": 100, 17 | "angle": 0, 18 | "x": 692.4692611694336, 19 | "y": 295.9705810546875, 20 | "strokeColor": "#1e1e1e", 21 | "backgroundColor": "#b2f2bb", 22 | "width": 688.4667968749999, 23 | "height": 257.822265625, 24 | "seed": 2033602904, 25 | "groupIds": [], 26 | "frameId": null, 27 | "roundness": { 28 | "type": 3 29 | }, 30 | "boundElements": [ 31 | { 32 | "id": "IeiUgpqDaJ6EM219tTpUi", 33 | "type": "arrow" 34 | }, 35 | { 36 | "id": "RhwK_pmxOK3u3m0nMb5wY", 37 | "type": "arrow" 38 | }, 39 | { 40 | "id": "C1Tgpb9QUodVmmMmlaWMO", 41 | "type": "arrow" 42 | } 43 | ], 44 | "updated": 1716798655930, 45 | "link": null, 46 | "locked": false 47 | }, 48 | { 49 | "type": "text", 50 | "version": 485, 51 | "versionNonce": 544266242, 52 | "isDeleted": false, 53 | "id": "XqvHY-IiOQrnp4kag1zoX", 54 | "fillStyle": "solid", 55 | "strokeWidth": 2, 56 | "strokeStyle": "dotted", 57 | "roughness": 0, 58 | "opacity": 100, 59 | "angle": 0, 60 | "x": 722.2142776042226, 61 | "y": 310.9118209726673, 62 | "strokeColor": "#1971c2", 63 | "backgroundColor": "transparent", 64 | "width": 500.667724609375, 65 | "height": 35, 66 | "seed": 1852861480, 67 | "groupIds": [], 68 | "frameId": null, 69 | "roundness": null, 70 | "boundElements": [], 71 | "updated": 1716811705985, 72 | "link": null, 73 | "locked": false, 74 | "fontSize": 28, 75 | "fontFamily": 1, 76 | "text": "LLM Application (e.g. with Langchain)", 77 | "textAlign": "right", 78 | "verticalAlign": "top", 79 | "containerId": null, 80 | "originalText": "LLM Application (e.g. with Langchain)", 81 | "lineHeight": 1.25, 82 | "baseline": 25 83 | }, 84 | { 85 | "type": "rectangle", 86 | "version": 364, 87 | "versionNonce": 2122907458, 88 | "isDeleted": false, 89 | "id": "cM2UKtDzIW0kitolm2gj4", 90 | "fillStyle": "solid", 91 | "strokeWidth": 2, 92 | "strokeStyle": "solid", 93 | "roughness": 0, 94 | "opacity": 100, 95 | "angle": 0, 96 | "x": 747.0053939819336, 97 | "y": 359.5418395996094, 98 | "strokeColor": "#1e1e1e", 99 | "backgroundColor": "#ffec99", 100 | "width": 195.22460937499997, 101 | "height": 143.12011718750003, 102 | "seed": 177825576, 103 | "groupIds": [], 104 | "frameId": null, 105 | "roundness": { 106 | "type": 3 107 | }, 108 | "boundElements": [ 109 | { 110 | "type": "text", 111 | "id": "jCbHza2-z9QqoKBP8ryBL" 112 | } 113 | ], 114 | "updated": 1716811709486, 115 | "link": null, 116 | "locked": false 117 | }, 118 | { 119 | "type": "text", 120 | "version": 169, 121 | "versionNonce": 569849346, 122 | "isDeleted": false, 123 | "id": "jCbHza2-z9QqoKBP8ryBL", 124 | "fillStyle": "solid", 125 | "strokeWidth": 2, 126 | "strokeStyle": "solid", 127 | "roughness": 0, 128 | "opacity": 100, 129 | "angle": 0, 130 | "x": 817.121711730957, 131 | "y": 413.6018981933594, 132 | "strokeColor": "#1e1e1e", 133 | "backgroundColor": "#ffec99", 134 | "width": 54.991973876953125, 135 | "height": 35, 136 | "seed": 653779496, 137 | "groupIds": [], 138 | "frameId": null, 139 | "roundness": null, 140 | "boundElements": [], 141 | "updated": 1716811712857, 142 | "link": null, 143 | "locked": false, 144 | "fontSize": 28, 145 | "fontFamily": 1, 146 | "text": "LLM", 147 | "textAlign": "center", 148 | "verticalAlign": "middle", 149 | "containerId": "cM2UKtDzIW0kitolm2gj4", 150 | "originalText": "LLM", 151 | "lineHeight": 1.25, 152 | "baseline": 25 153 | }, 154 | { 155 | "type": "rectangle", 156 | "version": 418, 157 | "versionNonce": 1236823390, 158 | "isDeleted": false, 159 | "id": "hHf-b4aiVCy-ZtHhzdhky", 160 | "fillStyle": "solid", 161 | "strokeWidth": 2, 162 | "strokeStyle": "solid", 163 | "roughness": 0, 164 | "opacity": 100, 165 | "angle": 0, 166 | "x": 1074.8960189819336, 167 | "y": 355.90985107421875, 168 | "strokeColor": "#1e1e1e", 169 | "backgroundColor": "#ffec99", 170 | "width": 165.92456054687491, 171 | "height": 146.46972656250003, 172 | "seed": 1792359512, 173 | "groupIds": [], 174 | "frameId": null, 175 | "roundness": { 176 | "type": 3 177 | }, 178 | "boundElements": [ 179 | { 180 | "type": "text", 181 | "id": "cSQjTiXO1jBIQVeVi7kZu" 182 | }, 183 | { 184 | "id": "cIQ4dzaLBxGg_o1Cp-Uel", 185 | "type": "arrow" 186 | }, 187 | { 188 | "id": "Gn2bRP6UvpW9MlFAf-kAs", 189 | "type": "arrow" 190 | }, 191 | { 192 | "id": "3IW_evXSge1LTyaEotOd3", 193 | "type": "arrow" 194 | } 195 | ], 196 | "updated": 1716811719927, 197 | "link": null, 198 | "locked": false 199 | }, 200 | { 201 | "type": "text", 202 | "version": 245, 203 | "versionNonce": 402461598, 204 | "isDeleted": false, 205 | "id": "cSQjTiXO1jBIQVeVi7kZu", 206 | "fillStyle": "solid", 207 | "strokeWidth": 2, 208 | "strokeStyle": "solid", 209 | "roughness": 0, 210 | "opacity": 100, 211 | "angle": 0, 212 | "x": 1083.252326965332, 213 | "y": 411.64471435546875, 214 | "strokeColor": "#1e1e1e", 215 | "backgroundColor": "#ffec99", 216 | "width": 149.21194458007812, 217 | "height": 35, 218 | "seed": 312209752, 219 | "groupIds": [], 220 | "frameId": null, 221 | "roundness": null, 222 | "boundElements": [], 223 | "updated": 1716811719928, 224 | "link": null, 225 | "locked": false, 226 | "fontSize": 28, 227 | "fontFamily": 1, 228 | "text": "MyScaleDB", 229 | "textAlign": "center", 230 | "verticalAlign": "middle", 231 | "containerId": "hHf-b4aiVCy-ZtHhzdhky", 232 | "originalText": "MyScaleDB", 233 | "lineHeight": 1.25, 234 | "baseline": 25 235 | }, 236 | { 237 | "type": "rectangle", 238 | "version": 1014, 239 | "versionNonce": 789802024, 240 | "isDeleted": false, 241 | "id": "CY7PPr8z_YSkWYBy_L2vm", 242 | "fillStyle": "hachure", 243 | "strokeWidth": 2, 244 | "strokeStyle": "dashed", 245 | "roughness": 0, 246 | "opacity": 100, 247 | "angle": 0, 248 | "x": 544.5345153808594, 249 | "y": 638.1583862304688, 250 | "strokeColor": "#1e1e1e", 251 | "backgroundColor": "#a5d8ff", 252 | "width": 1012.7441406250001, 253 | "height": 298.46679687499994, 254 | "seed": 1197912152, 255 | "groupIds": [], 256 | "frameId": null, 257 | "roundness": { 258 | "type": 3 259 | }, 260 | "boundElements": [ 261 | { 262 | "id": "MtUJJEjaRPR5l5H-H5fQV", 263 | "type": "arrow" 264 | } 265 | ], 266 | "updated": 1716799014128, 267 | "link": null, 268 | "locked": false 269 | }, 270 | { 271 | "type": "text", 272 | "version": 629, 273 | "versionNonce": 1870850462, 274 | "isDeleted": false, 275 | "id": "LS4Lzk2_NAYgWtPbp7krV", 276 | "fillStyle": "solid", 277 | "strokeWidth": 2, 278 | "strokeStyle": "dotted", 279 | "roughness": 0, 280 | "opacity": 100, 281 | "angle": 0, 282 | "x": 564.0494903117421, 283 | "y": 651.5006713867188, 284 | "strokeColor": "#1971c2", 285 | "backgroundColor": "transparent", 286 | "width": 255.555908203125, 287 | "height": 35, 288 | "seed": 1155458392, 289 | "groupIds": [], 290 | "frameId": null, 291 | "roundness": null, 292 | "boundElements": [], 293 | "updated": 1716811700649, 294 | "link": null, 295 | "locked": false, 296 | "fontSize": 28, 297 | "fontFamily": 1, 298 | "text": "MyScale Telemetry", 299 | "textAlign": "right", 300 | "verticalAlign": "top", 301 | "containerId": null, 302 | "originalText": "MyScale Telemetry", 303 | "lineHeight": 1.25, 304 | "baseline": 25 305 | }, 306 | { 307 | "type": "ellipse", 308 | "version": 126, 309 | "versionNonce": 372764968, 310 | "isDeleted": false, 311 | "id": "Geap4fsp-r3b-2V1X9wzx", 312 | "fillStyle": "solid", 313 | "strokeWidth": 2, 314 | "strokeStyle": "solid", 315 | "roughness": 0, 316 | "opacity": 100, 317 | "angle": 0, 318 | "x": 1504.7788314819336, 319 | "y": 152.25994873046875, 320 | "strokeColor": "#1e1e1e", 321 | "backgroundColor": "#a5d8ff", 322 | "width": 32.5634765625, 323 | "height": 33.80859375, 324 | "seed": 1096220456, 325 | "groupIds": [], 326 | "frameId": null, 327 | "roundness": { 328 | "type": 2 329 | }, 330 | "boundElements": [ 331 | { 332 | "id": "vZHEDN0QextjcX6LjPyAy", 333 | "type": "arrow" 334 | } 335 | ], 336 | "updated": 1716797918420, 337 | "link": null, 338 | "locked": false 339 | }, 340 | { 341 | "type": "line", 342 | "version": 234, 343 | "versionNonce": 888915752, 344 | "isDeleted": false, 345 | "id": "ahPN-QhUIm1tD0pa5U6lC", 346 | "fillStyle": "solid", 347 | "strokeWidth": 2, 348 | "strokeStyle": "solid", 349 | "roughness": 0, 350 | "opacity": 100, 351 | "angle": 0, 352 | "x": 1491.8637924194336, 353 | "y": 217.00769992277813, 354 | "strokeColor": "#1e1e1e", 355 | "backgroundColor": "#a5d8ff", 356 | "width": 71.50188631413843, 357 | "height": 2.1061496298093765, 358 | "seed": 1866916440, 359 | "groupIds": [], 360 | "frameId": null, 361 | "roundness": { 362 | "type": 2 363 | }, 364 | "boundElements": [], 365 | "updated": 1716797918420, 366 | "link": null, 367 | "locked": false, 368 | "startBinding": null, 369 | "endBinding": null, 370 | "lastCommittedPoint": null, 371 | "startArrowhead": null, 372 | "endArrowhead": null, 373 | "points": [ 374 | [ 375 | 0, 376 | 0 377 | ], 378 | [ 379 | 71.50188631413843, 380 | -2.1061496298093765 381 | ] 382 | ] 383 | }, 384 | { 385 | "type": "line", 386 | "version": 229, 387 | "versionNonce": 96405592, 388 | "isDeleted": false, 389 | "id": "vZHEDN0QextjcX6LjPyAy", 390 | "fillStyle": "solid", 391 | "strokeWidth": 2, 392 | "strokeStyle": "solid", 393 | "roughness": 0, 394 | "opacity": 100, 395 | "angle": 0, 396 | "x": 1521.9809799194336, 397 | "y": 187.84100341796875, 398 | "strokeColor": "#1e1e1e", 399 | "backgroundColor": "#a5d8ff", 400 | "width": 5.3564453125, 401 | "height": 56.9482421875, 402 | "seed": 897186392, 403 | "groupIds": [], 404 | "frameId": null, 405 | "roundness": { 406 | "type": 2 407 | }, 408 | "boundElements": [], 409 | "updated": 1716797918682, 410 | "link": null, 411 | "locked": false, 412 | "startBinding": { 413 | "elementId": "Geap4fsp-r3b-2V1X9wzx", 414 | "focus": 0.051120554632358435, 415 | "gap": 1.7967104247831749 416 | }, 417 | "endBinding": null, 418 | "lastCommittedPoint": null, 419 | "startArrowhead": null, 420 | "endArrowhead": null, 421 | "points": [ 422 | [ 423 | 0, 424 | 0 425 | ], 426 | [ 427 | 5.3564453125, 428 | 56.9482421875 429 | ] 430 | ] 431 | }, 432 | { 433 | "type": "line", 434 | "version": 194, 435 | "versionNonce": 1229069608, 436 | "isDeleted": false, 437 | "id": "m11fMkwHElbYtB0NHtzOu", 438 | "fillStyle": "solid", 439 | "strokeWidth": 2, 440 | "strokeStyle": "solid", 441 | "roughness": 0, 442 | "opacity": 100, 443 | "angle": 0, 444 | "x": 1527.3099531676153, 445 | "y": 246.80507745791795, 446 | "strokeColor": "#1e1e1e", 447 | "backgroundColor": "#a5d8ff", 448 | "width": 30.043748626318802, 449 | "height": 26.895901333890436, 450 | "seed": 682002776, 451 | "groupIds": [], 452 | "frameId": null, 453 | "roundness": { 454 | "type": 2 455 | }, 456 | "boundElements": [], 457 | "updated": 1716797918420, 458 | "link": null, 459 | "locked": false, 460 | "startBinding": null, 461 | "endBinding": null, 462 | "lastCommittedPoint": null, 463 | "startArrowhead": null, 464 | "endArrowhead": null, 465 | "points": [ 466 | [ 467 | 0, 468 | 0 469 | ], 470 | [ 471 | -30.043748626318802, 472 | 26.895901333890436 473 | ] 474 | ] 475 | }, 476 | { 477 | "type": "line", 478 | "version": 203, 479 | "versionNonce": 676608040, 480 | "isDeleted": false, 481 | "id": "yThZ7dbL52CzI7eqtlNGm", 482 | "fillStyle": "solid", 483 | "strokeWidth": 2, 484 | "strokeStyle": "solid", 485 | "roughness": 0, 486 | "opacity": 100, 487 | "angle": 0, 488 | "x": 1528.7748843336958, 489 | "y": 246.6496028695247, 490 | "strokeColor": "#1e1e1e", 491 | "backgroundColor": "#a5d8ff", 492 | "width": 46.56547058573769, 493 | "height": 27.09960367344407, 494 | "seed": 1204243544, 495 | "groupIds": [], 496 | "frameId": null, 497 | "roundness": { 498 | "type": 2 499 | }, 500 | "boundElements": [], 501 | "updated": 1716797918420, 502 | "link": null, 503 | "locked": false, 504 | "startBinding": null, 505 | "endBinding": null, 506 | "lastCommittedPoint": null, 507 | "startArrowhead": null, 508 | "endArrowhead": null, 509 | "points": [ 510 | [ 511 | 0, 512 | 0 513 | ], 514 | [ 515 | 46.56547058573769, 516 | 27.09960367344407 517 | ] 518 | ] 519 | }, 520 | { 521 | "type": "ellipse", 522 | "version": 294, 523 | "versionNonce": 1007809182, 524 | "isDeleted": false, 525 | "id": "YYJNckHhTOmLexjgzZPrR", 526 | "fillStyle": "solid", 527 | "strokeWidth": 2, 528 | "strokeStyle": "solid", 529 | "roughness": 0, 530 | "opacity": 100, 531 | "angle": 0, 532 | "x": 309.46797943115234, 533 | "y": 352.05078125, 534 | "strokeColor": "#1e1e1e", 535 | "backgroundColor": "#a5d8ff", 536 | "width": 32.5634765625, 537 | "height": 33.80859375, 538 | "seed": 1619372840, 539 | "groupIds": [], 540 | "frameId": null, 541 | "roundness": { 542 | "type": 2 543 | }, 544 | "boundElements": [ 545 | { 546 | "id": "0b39-dPMCJSI0sakqWEVL", 547 | "type": "arrow" 548 | } 549 | ], 550 | "updated": 1716811628035, 551 | "link": null, 552 | "locked": false 553 | }, 554 | { 555 | "type": "line", 556 | "version": 402, 557 | "versionNonce": 1074815774, 558 | "isDeleted": false, 559 | "id": "AKiiWJsD40i0inszTNYUJ", 560 | "fillStyle": "solid", 561 | "strokeWidth": 2, 562 | "strokeStyle": "solid", 563 | "roughness": 0, 564 | "opacity": 100, 565 | "angle": 0, 566 | "x": 296.55294036865234, 567 | "y": 416.79853244230935, 568 | "strokeColor": "#1e1e1e", 569 | "backgroundColor": "#a5d8ff", 570 | "width": 71.50188631413843, 571 | "height": 2.1061496298093765, 572 | "seed": 1811285544, 573 | "groupIds": [], 574 | "frameId": null, 575 | "roundness": { 576 | "type": 2 577 | }, 578 | "boundElements": [], 579 | "updated": 1716811628035, 580 | "link": null, 581 | "locked": false, 582 | "startBinding": null, 583 | "endBinding": null, 584 | "lastCommittedPoint": null, 585 | "startArrowhead": null, 586 | "endArrowhead": null, 587 | "points": [ 588 | [ 589 | 0, 590 | 0 591 | ], 592 | [ 593 | 71.50188631413843, 594 | -2.1061496298093765 595 | ] 596 | ] 597 | }, 598 | { 599 | "type": "line", 600 | "version": 563, 601 | "versionNonce": 210915166, 602 | "isDeleted": false, 603 | "id": "0b39-dPMCJSI0sakqWEVL", 604 | "fillStyle": "solid", 605 | "strokeWidth": 2, 606 | "strokeStyle": "solid", 607 | "roughness": 0, 608 | "opacity": 100, 609 | "angle": 0, 610 | "x": 326.67012786865234, 611 | "y": 387.6318359375, 612 | "strokeColor": "#1e1e1e", 613 | "backgroundColor": "#a5d8ff", 614 | "width": 5.3564453125, 615 | "height": 56.9482421875, 616 | "seed": 2124611880, 617 | "groupIds": [], 618 | "frameId": null, 619 | "roundness": { 620 | "type": 2 621 | }, 622 | "boundElements": [], 623 | "updated": 1716811628035, 624 | "link": null, 625 | "locked": false, 626 | "startBinding": { 627 | "elementId": "YYJNckHhTOmLexjgzZPrR", 628 | "focus": 0.051120554632358435, 629 | "gap": 1.7967104247831749 630 | }, 631 | "endBinding": null, 632 | "lastCommittedPoint": null, 633 | "startArrowhead": null, 634 | "endArrowhead": null, 635 | "points": [ 636 | [ 637 | 0, 638 | 0 639 | ], 640 | [ 641 | 5.3564453125, 642 | 56.9482421875 643 | ] 644 | ] 645 | }, 646 | { 647 | "type": "line", 648 | "version": 362, 649 | "versionNonce": 42775454, 650 | "isDeleted": false, 651 | "id": "8szlC2PKroLPq-XUww72O", 652 | "fillStyle": "solid", 653 | "strokeWidth": 2, 654 | "strokeStyle": "solid", 655 | "roughness": 0, 656 | "opacity": 100, 657 | "angle": 0, 658 | "x": 331.999101116834, 659 | "y": 446.5959099774492, 660 | "strokeColor": "#1e1e1e", 661 | "backgroundColor": "#a5d8ff", 662 | "width": 30.043748626318802, 663 | "height": 26.895901333890436, 664 | "seed": 1207475240, 665 | "groupIds": [], 666 | "frameId": null, 667 | "roundness": { 668 | "type": 2 669 | }, 670 | "boundElements": [], 671 | "updated": 1716811628035, 672 | "link": null, 673 | "locked": false, 674 | "startBinding": null, 675 | "endBinding": null, 676 | "lastCommittedPoint": null, 677 | "startArrowhead": null, 678 | "endArrowhead": null, 679 | "points": [ 680 | [ 681 | 0, 682 | 0 683 | ], 684 | [ 685 | -30.043748626318802, 686 | 26.895901333890436 687 | ] 688 | ] 689 | }, 690 | { 691 | "type": "line", 692 | "version": 371, 693 | "versionNonce": 1278250974, 694 | "isDeleted": false, 695 | "id": "-pNqKIKteJyIDOexALr5R", 696 | "fillStyle": "solid", 697 | "strokeWidth": 2, 698 | "strokeStyle": "solid", 699 | "roughness": 0, 700 | "opacity": 100, 701 | "angle": 0, 702 | "x": 333.46403228291456, 703 | "y": 446.4404353890559, 704 | "strokeColor": "#1e1e1e", 705 | "backgroundColor": "#a5d8ff", 706 | "width": 46.56547058573769, 707 | "height": 27.09960367344407, 708 | "seed": 702732072, 709 | "groupIds": [], 710 | "frameId": null, 711 | "roundness": { 712 | "type": 2 713 | }, 714 | "boundElements": [], 715 | "updated": 1716811628035, 716 | "link": null, 717 | "locked": false, 718 | "startBinding": null, 719 | "endBinding": null, 720 | "lastCommittedPoint": null, 721 | "startArrowhead": null, 722 | "endArrowhead": null, 723 | "points": [ 724 | [ 725 | 0, 726 | 0 727 | ], 728 | [ 729 | 46.56547058573769, 730 | 27.09960367344407 731 | ] 732 | ] 733 | }, 734 | { 735 | "type": "text", 736 | "version": 87, 737 | "versionNonce": 1945021506, 738 | "isDeleted": false, 739 | "id": "YP5svMsNThP-211ynyAd6", 740 | "fillStyle": "solid", 741 | "strokeWidth": 2, 742 | "strokeStyle": "solid", 743 | "roughness": 0, 744 | "opacity": 100, 745 | "angle": 0, 746 | "x": 1499.0969772338867, 747 | "y": 296.40057373046875, 748 | "strokeColor": "#1e1e1e", 749 | "backgroundColor": "#a5d8ff", 750 | "width": 62.551971435546875, 751 | "height": 35, 752 | "seed": 767242024, 753 | "groupIds": [], 754 | "frameId": null, 755 | "roundness": null, 756 | "boundElements": [], 757 | "updated": 1716811634871, 758 | "link": null, 759 | "locked": false, 760 | "fontSize": 28, 761 | "fontFamily": 1, 762 | "text": "User", 763 | "textAlign": "right", 764 | "verticalAlign": "top", 765 | "containerId": null, 766 | "originalText": "User", 767 | "lineHeight": 1.25, 768 | "baseline": 25 769 | }, 770 | { 771 | "type": "text", 772 | "version": 251, 773 | "versionNonce": 1070031426, 774 | "isDeleted": false, 775 | "id": "cV0JlaTeQx2ODgAzTkPvF", 776 | "fillStyle": "solid", 777 | "strokeWidth": 2, 778 | "strokeStyle": "solid", 779 | "roughness": 0, 780 | "opacity": 100, 781 | "angle": 0, 782 | "x": 292.5624313354492, 783 | "y": 499.66717529296875, 784 | "strokeColor": "#1e1e1e", 785 | "backgroundColor": "#a5d8ff", 786 | "width": 131.15194702148438, 787 | "height": 35, 788 | "seed": 2089856088, 789 | "groupIds": [], 790 | "frameId": null, 791 | "roundness": null, 792 | "boundElements": [], 793 | "updated": 1716811620807, 794 | "link": null, 795 | "locked": false, 796 | "fontSize": 28, 797 | "fontFamily": 1, 798 | "text": "Developer", 799 | "textAlign": "right", 800 | "verticalAlign": "top", 801 | "containerId": null, 802 | "originalText": "Developer", 803 | "lineHeight": 1.25, 804 | "baseline": 25 805 | }, 806 | { 807 | "type": "arrow", 808 | "version": 153, 809 | "versionNonce": 1518256216, 810 | "isDeleted": false, 811 | "id": "IeiUgpqDaJ6EM219tTpUi", 812 | "fillStyle": "solid", 813 | "strokeWidth": 2, 814 | "strokeStyle": "dotted", 815 | "roughness": 0, 816 | "opacity": 100, 817 | "angle": 0, 818 | "x": 433.9634017944336, 819 | "y": 422.05487060546875, 820 | "strokeColor": "#1e1e1e", 821 | "backgroundColor": "#a5d8ff", 822 | "width": 237.9443359375, 823 | "height": 2.5165350432902187, 824 | "seed": 1336742488, 825 | "groupIds": [], 826 | "frameId": null, 827 | "roundness": { 828 | "type": 2 829 | }, 830 | "boundElements": [], 831 | "updated": 1716798655930, 832 | "link": null, 833 | "locked": false, 834 | "startBinding": null, 835 | "endBinding": { 836 | "elementId": "m55XkYxvo2oab2q7saDF9", 837 | "focus": 0.06941817775224118, 838 | "gap": 20.5615234375 839 | }, 840 | "lastCommittedPoint": null, 841 | "startArrowhead": null, 842 | "endArrowhead": "arrow", 843 | "points": [ 844 | [ 845 | 0, 846 | 0 847 | ], 848 | [ 849 | 237.9443359375, 850 | -2.5165350432902187 851 | ] 852 | ] 853 | }, 854 | { 855 | "type": "text", 856 | "version": 78, 857 | "versionNonce": 1252072350, 858 | "isDeleted": false, 859 | "id": "eWtlo58kwwroJ-zA3_E09", 860 | "fillStyle": "solid", 861 | "strokeWidth": 2, 862 | "strokeStyle": "dotted", 863 | "roughness": 0, 864 | "opacity": 100, 865 | "angle": 0, 866 | "x": 538.8920211791992, 867 | "y": 379.8299255371094, 868 | "strokeColor": "#1e1e1e", 869 | "backgroundColor": "#a5d8ff", 870 | "width": 100.61990356445312, 871 | "height": 25, 872 | "seed": 21956184, 873 | "groupIds": [], 874 | "frameId": null, 875 | "roundness": null, 876 | "boundElements": [], 877 | "updated": 1716811746008, 878 | "link": null, 879 | "locked": false, 880 | "fontSize": 20, 881 | "fontFamily": 1, 882 | "text": "Build Apps", 883 | "textAlign": "right", 884 | "verticalAlign": "top", 885 | "containerId": null, 886 | "originalText": "Build Apps", 887 | "lineHeight": 1.25, 888 | "baseline": 18 889 | }, 890 | { 891 | "type": "arrow", 892 | "version": 200, 893 | "versionNonce": 1517614424, 894 | "isDeleted": false, 895 | "id": "RhwK_pmxOK3u3m0nMb5wY", 896 | "fillStyle": "solid", 897 | "strokeWidth": 2, 898 | "strokeStyle": "dotted", 899 | "roughness": 0, 900 | "opacity": 100, 901 | "angle": 0, 902 | "x": 1480.8921127319336, 903 | "y": 234.24237060546875, 904 | "strokeColor": "#1e1e1e", 905 | "backgroundColor": "#a5d8ff", 906 | "width": 95.39550781250023, 907 | "height": 63.42896613972289, 908 | "seed": 2034461528, 909 | "groupIds": [], 910 | "frameId": null, 911 | "roundness": { 912 | "type": 2 913 | }, 914 | "boundElements": [], 915 | "updated": 1716798655930, 916 | "link": null, 917 | "locked": false, 918 | "startBinding": null, 919 | "endBinding": { 920 | "elementId": "m55XkYxvo2oab2q7saDF9", 921 | "focus": 0.29263936206660085, 922 | "gap": 4.560546875 923 | }, 924 | "lastCommittedPoint": null, 925 | "startArrowhead": null, 926 | "endArrowhead": "arrow", 927 | "points": [ 928 | [ 929 | 0, 930 | 0 931 | ], 932 | [ 933 | -95.39550781250023, 934 | 63.42896613972289 935 | ] 936 | ] 937 | }, 938 | { 939 | "type": "text", 940 | "version": 166, 941 | "versionNonce": 1016761858, 942 | "isDeleted": false, 943 | "id": "qbKyYo-x0e9l7SbXwmxJf", 944 | "fillStyle": "solid", 945 | "strokeWidth": 2, 946 | "strokeStyle": "dotted", 947 | "roughness": 0, 948 | "opacity": 100, 949 | "angle": 0, 950 | "x": 1354.4699700295926, 951 | "y": 221.79119873046875, 952 | "strokeColor": "#1e1e1e", 953 | "backgroundColor": "#a5d8ff", 954 | "width": 96.61991882324219, 955 | "height": 25, 956 | "seed": 883820376, 957 | "groupIds": [], 958 | "frameId": null, 959 | "roundness": null, 960 | "boundElements": [], 961 | "updated": 1716811743680, 962 | "link": null, 963 | "locked": false, 964 | "fontSize": 20, 965 | "fontFamily": 1, 966 | "text": "Interacts", 967 | "textAlign": "right", 968 | "verticalAlign": "top", 969 | "containerId": null, 970 | "originalText": "Interacts", 971 | "lineHeight": 1.25, 972 | "baseline": 18 973 | }, 974 | { 975 | "type": "rectangle", 976 | "version": 323, 977 | "versionNonce": 745964120, 978 | "isDeleted": false, 979 | "id": "vP7_P2-R32jjcrOhoIOTn", 980 | "fillStyle": "solid", 981 | "strokeWidth": 2, 982 | "strokeStyle": "solid", 983 | "roughness": 0, 984 | "opacity": 100, 985 | "angle": 0, 986 | "x": 958.5678939819336, 987 | "y": 734.6915893554688, 988 | "strokeColor": "#1e1e1e", 989 | "backgroundColor": "#a5d8ff", 990 | "width": 227.70507812499997, 991 | "height": 131.4404296875, 992 | "seed": 729113688, 993 | "groupIds": [], 994 | "frameId": null, 995 | "roundness": { 996 | "type": 3 997 | }, 998 | "boundElements": [ 999 | { 1000 | "type": "text", 1001 | "id": "05xdP6rUvbnHfEc2t_rMA" 1002 | }, 1003 | { 1004 | "id": "Gn2bRP6UvpW9MlFAf-kAs", 1005 | "type": "arrow" 1006 | } 1007 | ], 1008 | "updated": 1716798734459, 1009 | "link": null, 1010 | "locked": false 1011 | }, 1012 | { 1013 | "type": "text", 1014 | "version": 270, 1015 | "versionNonce": 1580527960, 1016 | "isDeleted": false, 1017 | "id": "05xdP6rUvbnHfEc2t_rMA", 1018 | "fillStyle": "solid", 1019 | "strokeWidth": 2, 1020 | "strokeStyle": "solid", 1021 | "roughness": 0, 1022 | "opacity": 100, 1023 | "angle": 0, 1024 | "x": 991.1904754638672, 1025 | "y": 775.4118041992188, 1026 | "strokeColor": "#1e1e1e", 1027 | "backgroundColor": "#a5d8ff", 1028 | "width": 162.4599151611328, 1029 | "height": 50, 1030 | "seed": 529872936, 1031 | "groupIds": [], 1032 | "frameId": null, 1033 | "roundness": null, 1034 | "boundElements": [], 1035 | "updated": 1716799053753, 1036 | "link": null, 1037 | "locked": false, 1038 | "fontSize": 20, 1039 | "fontFamily": 1, 1040 | "text": "Granafa Trace \nDashboard", 1041 | "textAlign": "center", 1042 | "verticalAlign": "middle", 1043 | "containerId": "vP7_P2-R32jjcrOhoIOTn", 1044 | "originalText": "Granafa Trace Dashboard", 1045 | "lineHeight": 1.25, 1046 | "baseline": 44 1047 | }, 1048 | { 1049 | "type": "rectangle", 1050 | "version": 157, 1051 | "versionNonce": 28338216, 1052 | "isDeleted": false, 1053 | "id": "i-zDEg97lCZY36wieSjVS", 1054 | "fillStyle": "solid", 1055 | "strokeWidth": 2, 1056 | "strokeStyle": "solid", 1057 | "roughness": 0, 1058 | "opacity": 100, 1059 | "angle": 0, 1060 | "x": 1269.9594955444336, 1061 | "y": 730.2140502929688, 1062 | "strokeColor": "#1e1e1e", 1063 | "backgroundColor": "#a5d8ff", 1064 | "width": 203.2373046875, 1065 | "height": 129.423828125, 1066 | "seed": 128933208, 1067 | "groupIds": [], 1068 | "frameId": null, 1069 | "roundness": { 1070 | "type": 3 1071 | }, 1072 | "boundElements": [ 1073 | { 1074 | "type": "text", 1075 | "id": "XgNSWcqOoa1Sazff09rMY" 1076 | }, 1077 | { 1078 | "id": "3IW_evXSge1LTyaEotOd3", 1079 | "type": "arrow" 1080 | } 1081 | ], 1082 | "updated": 1716798759280, 1083 | "link": null, 1084 | "locked": false 1085 | }, 1086 | { 1087 | "type": "text", 1088 | "version": 85, 1089 | "versionNonce": 13594664, 1090 | "isDeleted": false, 1091 | "id": "XgNSWcqOoa1Sazff09rMY", 1092 | "fillStyle": "solid", 1093 | "strokeWidth": 2, 1094 | "strokeStyle": "solid", 1095 | "roughness": 0, 1096 | "opacity": 100, 1097 | "angle": 0, 1098 | "x": 1284.3482055664062, 1099 | "y": 782.4259643554688, 1100 | "strokeColor": "#1e1e1e", 1101 | "backgroundColor": "#a5d8ff", 1102 | "width": 174.4598846435547, 1103 | "height": 25, 1104 | "seed": 1773324072, 1105 | "groupIds": [], 1106 | "frameId": null, 1107 | "roundness": null, 1108 | "boundElements": [], 1109 | "updated": 1716799053753, 1110 | "link": null, 1111 | "locked": false, 1112 | "fontSize": 20, 1113 | "fontFamily": 1, 1114 | "text": "Ragas Evaluation", 1115 | "textAlign": "center", 1116 | "verticalAlign": "middle", 1117 | "containerId": "i-zDEg97lCZY36wieSjVS", 1118 | "originalText": "Ragas Evaluation", 1119 | "lineHeight": 1.25, 1120 | "baseline": 19 1121 | }, 1122 | { 1123 | "type": "rectangle", 1124 | "version": 279, 1125 | "versionNonce": 1669535272, 1126 | "isDeleted": false, 1127 | "id": "m0e-elagUMpUpUTaPtCdJ", 1128 | "fillStyle": "solid", 1129 | "strokeWidth": 2, 1130 | "strokeStyle": "solid", 1131 | "roughness": 0, 1132 | "opacity": 100, 1133 | "angle": 0, 1134 | "x": 618.7729721069336, 1135 | "y": 736.5079956054688, 1136 | "strokeColor": "#1e1e1e", 1137 | "backgroundColor": "#a5d8ff", 1138 | "width": 256.40624999999994, 1139 | "height": 128.96972656249997, 1140 | "seed": 1602913112, 1141 | "groupIds": [], 1142 | "frameId": null, 1143 | "roundness": { 1144 | "type": 3 1145 | }, 1146 | "boundElements": [ 1147 | { 1148 | "type": "text", 1149 | "id": "CpyUtz6k5tAXY6A8xjt2O" 1150 | }, 1151 | { 1152 | "id": "cIQ4dzaLBxGg_o1Cp-Uel", 1153 | "type": "arrow" 1154 | }, 1155 | { 1156 | "id": "C1Tgpb9QUodVmmMmlaWMO", 1157 | "type": "arrow" 1158 | } 1159 | ], 1160 | "updated": 1716798655930, 1161 | "link": null, 1162 | "locked": false 1163 | }, 1164 | { 1165 | "type": "text", 1166 | "version": 178, 1167 | "versionNonce": 1245513304, 1168 | "isDeleted": false, 1169 | "id": "CpyUtz6k5tAXY6A8xjt2O", 1170 | "fillStyle": "solid", 1171 | "strokeWidth": 2, 1172 | "strokeStyle": "solid", 1173 | "roughness": 0, 1174 | "opacity": 100, 1175 | "angle": 0, 1176 | "x": 628.1161880493164, 1177 | "y": 788.4928588867188, 1178 | "strokeColor": "#1e1e1e", 1179 | "backgroundColor": "#a5d8ff", 1180 | "width": 237.71981811523438, 1181 | "height": 25, 1182 | "seed": 1272446248, 1183 | "groupIds": [], 1184 | "frameId": null, 1185 | "roundness": null, 1186 | "boundElements": [], 1187 | "updated": 1716799053753, 1188 | "link": null, 1189 | "locked": false, 1190 | "fontSize": 20, 1191 | "fontFamily": 1, 1192 | "text": "MyScaleCallbackHadndler", 1193 | "textAlign": "center", 1194 | "verticalAlign": "middle", 1195 | "containerId": "m0e-elagUMpUpUTaPtCdJ", 1196 | "originalText": "MyScaleCallbackHadndler", 1197 | "lineHeight": 1.25, 1198 | "baseline": 19 1199 | }, 1200 | { 1201 | "type": "arrow", 1202 | "version": 593, 1203 | "versionNonce": 1230012712, 1204 | "isDeleted": false, 1205 | "id": "C1Tgpb9QUodVmmMmlaWMO", 1206 | "fillStyle": "solid", 1207 | "strokeWidth": 2, 1208 | "strokeStyle": "solid", 1209 | "roughness": 0, 1210 | "opacity": 100, 1211 | "angle": 0, 1212 | "x": 1014.5298080444336, 1213 | "y": 554.4376831054688, 1214 | "strokeColor": "#1e1e1e", 1215 | "backgroundColor": "#a5d8ff", 1216 | "width": 275.400390625, 1217 | "height": 182.3193359375, 1218 | "seed": 233487912, 1219 | "groupIds": [], 1220 | "frameId": null, 1221 | "roundness": { 1222 | "type": 2 1223 | }, 1224 | "boundElements": [], 1225 | "updated": 1716798710074, 1226 | "link": null, 1227 | "locked": false, 1228 | "startBinding": { 1229 | "elementId": "m55XkYxvo2oab2q7saDF9", 1230 | "focus": -0.32196615089547476, 1231 | "gap": 1 1232 | }, 1233 | "endBinding": { 1234 | "elementId": "m0e-elagUMpUpUTaPtCdJ", 1235 | "focus": -0.46486146228820535, 1236 | "gap": 1 1237 | }, 1238 | "lastCommittedPoint": null, 1239 | "startArrowhead": null, 1240 | "endArrowhead": "arrow", 1241 | "points": [ 1242 | [ 1243 | 0, 1244 | 0 1245 | ], 1246 | [ 1247 | -275.400390625, 1248 | 182.3193359375 1249 | ] 1250 | ] 1251 | }, 1252 | { 1253 | "type": "text", 1254 | "version": 212, 1255 | "versionNonce": 2122894366, 1256 | "isDeleted": false, 1257 | "id": "90g5z61HMpl_OAo7HuM_e", 1258 | "fillStyle": "solid", 1259 | "strokeWidth": 2, 1260 | "strokeStyle": "solid", 1261 | "roughness": 0, 1262 | "opacity": 100, 1263 | "angle": 5.7289727256559395, 1264 | "x": 829.5588760375977, 1265 | "y": 602.0304565429688, 1266 | "strokeColor": "#1e1e1e", 1267 | "backgroundColor": "#a5d8ff", 1268 | "width": 96.67991638183594, 1269 | "height": 25, 1270 | "seed": 442581288, 1271 | "groupIds": [], 1272 | "frameId": null, 1273 | "roundness": null, 1274 | "boundElements": [], 1275 | "updated": 1716811607405, 1276 | "link": null, 1277 | "locked": false, 1278 | "fontSize": 20, 1279 | "fontFamily": 1, 1280 | "text": "Integrate", 1281 | "textAlign": "right", 1282 | "verticalAlign": "top", 1283 | "containerId": null, 1284 | "originalText": "Integrate", 1285 | "lineHeight": 1.25, 1286 | "baseline": 18 1287 | }, 1288 | { 1289 | "type": "arrow", 1290 | "version": 263, 1291 | "versionNonce": 1894198750, 1292 | "isDeleted": false, 1293 | "id": "cIQ4dzaLBxGg_o1Cp-Uel", 1294 | "fillStyle": "solid", 1295 | "strokeWidth": 2, 1296 | "strokeStyle": "solid", 1297 | "roughness": 0, 1298 | "opacity": 100, 1299 | "angle": 0, 1300 | "x": 789.6183717035747, 1301 | "y": 735.5079956054688, 1302 | "strokeColor": "#1e1e1e", 1303 | "backgroundColor": "#a5d8ff", 1304 | "width": 360.48398685891743, 1305 | "height": 230.708984375, 1306 | "seed": 1633756200, 1307 | "groupIds": [], 1308 | "frameId": null, 1309 | "roundness": { 1310 | "type": 2 1311 | }, 1312 | "boundElements": [], 1313 | "updated": 1716811719927, 1314 | "link": null, 1315 | "locked": false, 1316 | "startBinding": { 1317 | "elementId": "m0e-elagUMpUpUTaPtCdJ", 1318 | "gap": 1, 1319 | "focus": -0.2609709795358578 1320 | }, 1321 | "endBinding": { 1322 | "elementId": "hHf-b4aiVCy-ZtHhzdhky", 1323 | "gap": 2.41943359375, 1324 | "focus": -0.5595678442566574 1325 | }, 1326 | "lastCommittedPoint": null, 1327 | "startArrowhead": null, 1328 | "endArrowhead": "arrow", 1329 | "points": [ 1330 | [ 1331 | 0, 1332 | 0 1333 | ], 1334 | [ 1335 | 360.48398685891743, 1336 | -230.708984375 1337 | ] 1338 | ] 1339 | }, 1340 | { 1341 | "type": "text", 1342 | "version": 411, 1343 | "versionNonce": 213685150, 1344 | "isDeleted": false, 1345 | "id": "gw0H1IjOxE9KS2ZpR0ghy", 1346 | "fillStyle": "solid", 1347 | "strokeWidth": 2, 1348 | "strokeStyle": "dotted", 1349 | "roughness": 0, 1350 | "opacity": 100, 1351 | "angle": 5.6561451783099415, 1352 | "x": 887.3551559448241, 1353 | "y": 633.7754682107819, 1354 | "strokeColor": "#1e1e1e", 1355 | "backgroundColor": "#a5d8ff", 1356 | "width": 122.57992553710938, 1357 | "height": 25, 1358 | "seed": 981455144, 1359 | "groupIds": [], 1360 | "frameId": null, 1361 | "roundness": null, 1362 | "boundElements": [], 1363 | "updated": 1716811600025, 1364 | "link": null, 1365 | "locked": false, 1366 | "fontSize": 20, 1367 | "fontFamily": 1, 1368 | "text": "Trace Data", 1369 | "textAlign": "right", 1370 | "verticalAlign": "top", 1371 | "containerId": null, 1372 | "originalText": "Trace Data", 1373 | "lineHeight": 1.25, 1374 | "baseline": 18 1375 | }, 1376 | { 1377 | "type": "arrow", 1378 | "version": 97, 1379 | "versionNonce": 1656737374, 1380 | "isDeleted": false, 1381 | "id": "Gn2bRP6UvpW9MlFAf-kAs", 1382 | "fillStyle": "solid", 1383 | "strokeWidth": 2, 1384 | "strokeStyle": "solid", 1385 | "roughness": 0, 1386 | "opacity": 100, 1387 | "angle": 0, 1388 | "x": 1160.707114371464, 1389 | "y": 507.21112060546886, 1390 | "strokeColor": "#1e1e1e", 1391 | "backgroundColor": "#a5d8ff", 1392 | "width": 91.03235405047508, 1393 | "height": 226.4804687499999, 1394 | "seed": 370678104, 1395 | "groupIds": [], 1396 | "frameId": null, 1397 | "roundness": { 1398 | "type": 2 1399 | }, 1400 | "boundElements": [], 1401 | "updated": 1716811719927, 1402 | "link": null, 1403 | "locked": false, 1404 | "startBinding": { 1405 | "elementId": "hHf-b4aiVCy-ZtHhzdhky", 1406 | "gap": 4.83154296875, 1407 | "focus": -0.30439977621478076 1408 | }, 1409 | "endBinding": { 1410 | "elementId": "vP7_P2-R32jjcrOhoIOTn", 1411 | "gap": 1, 1412 | "focus": -0.21076336420598207 1413 | }, 1414 | "lastCommittedPoint": null, 1415 | "startArrowhead": null, 1416 | "endArrowhead": "arrow", 1417 | "points": [ 1418 | [ 1419 | 0, 1420 | 0 1421 | ], 1422 | [ 1423 | -91.03235405047508, 1424 | 226.4804687499999 1425 | ] 1426 | ] 1427 | }, 1428 | { 1429 | "type": "text", 1430 | "version": 92, 1431 | "versionNonce": 519437762, 1432 | "isDeleted": false, 1433 | "id": "bzXfa888YmRqzDCavNkEN", 1434 | "fillStyle": "solid", 1435 | "strokeWidth": 2, 1436 | "strokeStyle": "solid", 1437 | "roughness": 0, 1438 | "opacity": 100, 1439 | "angle": 0, 1440 | "x": 1125.3564834594727, 1441 | "y": 590.5558471679688, 1442 | "strokeColor": "#1e1e1e", 1443 | "backgroundColor": "#a5d8ff", 1444 | "width": 82.29991149902344, 1445 | "height": 25, 1446 | "seed": 1286149464, 1447 | "groupIds": [], 1448 | "frameId": null, 1449 | "roundness": null, 1450 | "boundElements": [], 1451 | "updated": 1716811603320, 1452 | "link": null, 1453 | "locked": false, 1454 | "fontSize": 20, 1455 | "fontFamily": 1, 1456 | "text": "Visualize", 1457 | "textAlign": "right", 1458 | "verticalAlign": "top", 1459 | "containerId": null, 1460 | "originalText": "Visualize", 1461 | "lineHeight": 1.25, 1462 | "baseline": 18 1463 | }, 1464 | { 1465 | "type": "arrow", 1466 | "version": 130, 1467 | "versionNonce": 642404062, 1468 | "isDeleted": false, 1469 | "id": "3IW_evXSge1LTyaEotOd3", 1470 | "fillStyle": "solid", 1471 | "strokeWidth": 2, 1472 | "strokeStyle": "solid", 1473 | "roughness": 0, 1474 | "opacity": 100, 1475 | "angle": 0, 1476 | "x": 1160.464404957623, 1477 | "y": 506.40057373046875, 1478 | "strokeColor": "#1e1e1e", 1479 | "backgroundColor": "#a5d8ff", 1480 | "width": 209.7733055859842, 1481 | "height": 221.9921875, 1482 | "seed": 1219036712, 1483 | "groupIds": [], 1484 | "frameId": null, 1485 | "roundness": { 1486 | "type": 2 1487 | }, 1488 | "boundElements": [], 1489 | "updated": 1716811719927, 1490 | "link": null, 1491 | "locked": false, 1492 | "startBinding": { 1493 | "elementId": "hHf-b4aiVCy-ZtHhzdhky", 1494 | "gap": 4.02099609375, 1495 | "focus": 0.4626593061800176 1496 | }, 1497 | "endBinding": { 1498 | "elementId": "i-zDEg97lCZY36wieSjVS", 1499 | "gap": 1.8212890625, 1500 | "focus": 0.3780250752130202 1501 | }, 1502 | "lastCommittedPoint": null, 1503 | "startArrowhead": null, 1504 | "endArrowhead": "arrow", 1505 | "points": [ 1506 | [ 1507 | 0, 1508 | 0 1509 | ], 1510 | [ 1511 | 209.7733055859842, 1512 | 221.9921875 1513 | ] 1514 | ] 1515 | }, 1516 | { 1517 | "type": "text", 1518 | "version": 54, 1519 | "versionNonce": 1294945886, 1520 | "isDeleted": false, 1521 | "id": "7Cj9sFh_8z47RFX7LZUjS", 1522 | "fillStyle": "solid", 1523 | "strokeWidth": 2, 1524 | "strokeStyle": "solid", 1525 | "roughness": 0, 1526 | "opacity": 100, 1527 | "angle": 0, 1528 | "x": 1261.4170608520508, 1529 | "y": 588.5831909179688, 1530 | "strokeColor": "#1e1e1e", 1531 | "backgroundColor": "#a5d8ff", 1532 | "width": 89.47993469238281, 1533 | "height": 25, 1534 | "seed": 417768744, 1535 | "groupIds": [], 1536 | "frameId": null, 1537 | "roundness": null, 1538 | "boundElements": [], 1539 | "updated": 1716811594282, 1540 | "link": null, 1541 | "locked": false, 1542 | "fontSize": 20, 1543 | "fontFamily": 1, 1544 | "text": "Evaluate", 1545 | "textAlign": "right", 1546 | "verticalAlign": "top", 1547 | "containerId": null, 1548 | "originalText": "Evaluate", 1549 | "lineHeight": 1.25, 1550 | "baseline": 18 1551 | }, 1552 | { 1553 | "type": "arrow", 1554 | "version": 203, 1555 | "versionNonce": 149782568, 1556 | "isDeleted": false, 1557 | "id": "MtUJJEjaRPR5l5H-H5fQV", 1558 | "fillStyle": "solid", 1559 | "strokeWidth": 2, 1560 | "strokeStyle": "dotted", 1561 | "roughness": 0, 1562 | "opacity": 100, 1563 | "angle": 0, 1564 | "x": 425.9165267944336, 1565 | "y": 429.36444091796875, 1566 | "strokeColor": "#1e1e1e", 1567 | "backgroundColor": "#a5d8ff", 1568 | "width": 114.443359375, 1569 | "height": 339.6630859374998, 1570 | "seed": 644018264, 1571 | "groupIds": [], 1572 | "frameId": null, 1573 | "roundness": { 1574 | "type": 2 1575 | }, 1576 | "boundElements": [], 1577 | "updated": 1716798919755, 1578 | "link": null, 1579 | "locked": false, 1580 | "startBinding": null, 1581 | "endBinding": { 1582 | "elementId": "CY7PPr8z_YSkWYBy_L2vm", 1583 | "focus": -0.9060557861890977, 1584 | "gap": 4.174629211425781 1585 | }, 1586 | "lastCommittedPoint": null, 1587 | "startArrowhead": null, 1588 | "endArrowhead": "arrow", 1589 | "points": [ 1590 | [ 1591 | 0, 1592 | 0 1593 | ], 1594 | [ 1595 | 114.443359375, 1596 | 339.6630859374998 1597 | ] 1598 | ] 1599 | }, 1600 | { 1601 | "type": "text", 1602 | "version": 416, 1603 | "versionNonce": 1366789314, 1604 | "isDeleted": false, 1605 | "id": "BXW0Wkm-xKd4Sf808SUTz", 1606 | "fillStyle": "solid", 1607 | "strokeWidth": 2, 1608 | "strokeStyle": "dotted", 1609 | "roughness": 0, 1610 | "opacity": 100, 1611 | "angle": 6.281339120850776, 1612 | "x": 302.6008071899414, 1613 | "y": 598.9201049804688, 1614 | "strokeColor": "#1e1e1e", 1615 | "backgroundColor": "#a5d8ff", 1616 | "width": 163.5598602294922, 1617 | "height": 25, 1618 | "seed": 349804072, 1619 | "groupIds": [], 1620 | "frameId": null, 1621 | "roundness": null, 1622 | "boundElements": [], 1623 | "updated": 1716811748151, 1624 | "link": null, 1625 | "locked": false, 1626 | "fontSize": 20, 1627 | "fontFamily": 1, 1628 | "text": "Debug & Monitor", 1629 | "textAlign": "right", 1630 | "verticalAlign": "top", 1631 | "containerId": null, 1632 | "originalText": "Debug & Monitor", 1633 | "lineHeight": 1.25, 1634 | "baseline": 18 1635 | } 1636 | ], 1637 | "appState": { 1638 | "gridSize": null, 1639 | "viewBackgroundColor": "#ffffff" 1640 | }, 1641 | "files": {} 1642 | } --------------------------------------------------------------------------------