diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 54ceb4e9e..5c73aa5b8 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -63,6 +63,7 @@ jobs: OSS_ACCESS_KEY_SECRET: ${{ secrets.OSS_ACCESS_KEY_SECRET }} TRACE_TOKEN: ${{ secrets.TRACE_TOKEN }} DISABLE_REDIS_CACHE_IN_TESTS: "true" + ENCRYPTION_KEY: ${{ secrets.ENCRYPTION_KEY }} - name: Get Cover uses: orgoro/coverage@v3.1 diff --git a/backend/app/main.py b/backend/app/main.py index c9318b2d1..5c779709a 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -81,6 +81,7 @@ def create_app(): from app.log_middleware import CustomLoggingMiddleware from fastapi.middleware.cors import CORSMiddleware from api.request_validate_exception import validation_exception_handler + from extensions.trace.base import setup_propagator app = FastAPI(lifespan=lifespan) add_config_router(app) @@ -98,6 +99,7 @@ def create_app(): allow_credentials=False, ) app.add_middleware(CustomLoggingMiddleware) + setup_propagator(app) app.add_exception_handler(ApiException, api_exception_handler) app.add_exception_handler(RequestValidationError, validation_exception_handler) return app diff --git a/backend/common/tool/search_result.py b/backend/common/tool/search_result.py index b0ab24966..24dd1443e 100644 --- a/backend/common/tool/search_result.py +++ b/backend/common/tool/search_result.py @@ -3,6 +3,7 @@ class SearchResult(BaseModel): + id: str | None = None title: str | None = None content: str | None = None url: str | None = None diff --git a/backend/db/models/trace.py b/backend/db/models/trace.py index a0971638d..4cfaf8f86 100644 --- a/backend/db/models/trace.py +++ b/backend/db/models/trace.py @@ -17,6 +17,6 @@ class TraceModel(SQLModel): class TraceModelEntity(TraceModel, table=True): __tablename__ = "pai_trace_config" - id: str = Field(default=lambda x: str(uuid.uuid4().hex), primary_key=True) + id: str = Field(default_factory=lambda x: str(uuid.uuid4().hex), primary_key=True) def is_enabled(self) -> bool: - return self.enabled and self.service_name and self.token and self.endpoint + return self.enabled and self.service_name and self.endpoint diff --git a/backend/extensions/trace/baggage_processor.py b/backend/extensions/trace/baggage_processor.py new file mode 100644 index 000000000..c82c6ba78 --- /dev/null +++ b/backend/extensions/trace/baggage_processor.py @@ -0,0 +1,133 @@ +# Copyright The OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Optional, Set + +from opentelemetry.baggage import get_all as get_all_baggage +from opentelemetry.context import Context +from opentelemetry.sdk.trace import SpanProcessor +from opentelemetry.trace import Span + + +DEFAULT_ALLOWED_PREFIXES = set(["traffic.llm_sdk."]) +DEFAULT_STRIP_PREFIXES = set(["traffic.llm_sdk."]) + + +class LoongSuiteBaggageSpanProcessor(SpanProcessor): + """ + LoongSuite Baggage Span Processor + + Reads Baggage entries from the parent context and adds matching baggage + key-value pairs to span attributes based on configured prefix matching rules. + + Supported features: + 1. Prefix matching: Only process baggage keys that match specified prefixes + 2. Prefix stripping: Remove specified prefixes before writing to attributes + + Example: + # Configure matching prefixes: "traffic.", "app." + # Configure stripping prefix: "traffic." + # baggage: traffic.hello_key = "value" + # Result: attributes will have hello_key = "value" (prefix stripped) + + # baggage: app.user_id = "123" + # Result: attributes will have app.user_id = "123" (app. prefix not stripped) + + ⚠ Warning ⚠️ + + Do not put sensitive information in Baggage. + + To repeat: a consequence of adding data to Baggage is that the keys and + values will appear in all outgoing HTTP headers from the application. + """ + + def __init__( + self, + allowed_prefixes: Optional[Set[str]] = DEFAULT_ALLOWED_PREFIXES, + strip_prefixes: Optional[Set[str]] = DEFAULT_STRIP_PREFIXES, + ) -> None: + """ + Initialize LoongSuite Baggage Span Processor + + Args: + allowed_prefixes: Set of allowed baggage key prefixes. If None or empty, + all baggage keys are allowed. If specified, only keys + matching these prefixes will be processed. + strip_prefixes: Set of prefixes to strip. If a baggage key matches these + prefixes, they will be removed before writing to attributes. + """ + self._allowed_prefixes = allowed_prefixes or [] + self._strip_prefixes = strip_prefixes or set() + + # If allowed_prefixes is empty, allow all prefixes + self._allow_all = len(self._allowed_prefixes) == 0 + + def _should_process_key(self, key: str) -> bool: + """ + Determine whether this baggage key should be processed + + Args: + key: baggage key + + Returns: + True if the key should be processed, False otherwise + """ + if self._allow_all: + return True + + # Check if key matches any of the allowed prefixes + for prefix in self._allowed_prefixes: + if key.startswith(prefix): + return True + + return False + + def _strip_prefix(self, key: str) -> str: + """ + Strip matching prefix from key + + Args: + key: original baggage key + + Returns: + key with prefix stripped + """ + for prefix in self._strip_prefixes: + if key.startswith(prefix): + return key[len(prefix) :] + return key + + def on_start( + self, span: "Span", parent_context: Optional[Context] = None + ) -> None: + """ + Called when a span starts, adds matching baggage entries to span attributes + + Args: + span: span to add attributes to + parent_context: parent context used to retrieve baggage + """ + baggage = get_all_baggage(parent_context) + + for key, value in baggage.items(): + # Check if this key should be processed + if not self._should_process_key(key): + continue + + # Strip prefix if needed + attribute_key = self._strip_prefix(key) + + # Add to span attributes + # Baggage values are strings, which are valid AttributeValue + span.set_attribute(attribute_key, value) # type: ignore[arg-type] diff --git a/backend/extensions/trace/base.py b/backend/extensions/trace/base.py index 91ee17ea9..300fa6e56 100644 --- a/backend/extensions/trace/base.py +++ b/backend/extensions/trace/base.py @@ -1,7 +1,7 @@ import os import socket from functools import wraps -from typing import Callable, AsyncGenerator +from typing import Callable, AsyncGenerator, Union from loguru import logger from opentelemetry import trace @@ -14,21 +14,41 @@ from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import ( BatchSpanProcessor, + ConsoleSpanExporter, + SimpleSpanProcessor, ) from opentelemetry.trace import Span from opentelemetry.context import attach, detach from openinference.instrumentation.openai import OpenAIInstrumentor from openinference.semconv.trace import SpanAttributes, MessageAttributes, MessageContentAttributes -from extensions.trace.reloadable_exporter import ReloadableOTLPSpanExporter +from extensions.trace.grpc_exporter import ReloadableGrpcOTLPSpanExporter +from extensions.trace.http_exporter import ReloadableHttpOTLPSpanExporter from extensions.trace import context as trace_context from extensions.trace.trace_config import TraceConfig +from opentelemetry.propagate import set_global_textmap +from opentelemetry.trace.propagation.tracecontext import TraceContextTextMapPropagator +from opentelemetry.baggage.propagation import W3CBaggagePropagator +from opentelemetry.propagators.composite import CompositePropagator +from extensions.trace.baggage_processor import LoongSuiteBaggageSpanProcessor +from extensions.trace.trace_context_middleware import TraceContextMiddleware + + +ENABLE_TRACE_DEBUG = os.getenv("ENABLE_TRACE_DEBUG", "false").lower() in ["true", "1", "yes", "y"] + + +def setup_propagator(app): + set_global_textmap(CompositePropagator([ + TraceContextTextMapPropagator(), # 处理 traceparent + W3CBaggagePropagator() # 处理 baggage + ])) + app.add_middleware(TraceContextMiddleware) # trace_provider为singleton, 不支持覆盖,故修改trace配置时,默认覆盖exporter和resource # 这样如果用户填错密码,还可以成功刷新 trace_config: TraceConfig = None -exporter: ReloadableOTLPSpanExporter = None +exporter: Union[ReloadableGrpcOTLPSpanExporter, ReloadableHttpOTLPSpanExporter] = None resource: Resource = None trace_provider: TracerProvider = None @@ -49,17 +69,13 @@ def init_instrument(config: TraceConfig): if config.user_args: trace_context.init_custom_context(config.user_args.values()) - grpc_endpoint = config.endpoint + trace_endpoint = config.endpoint token = config.token service_name = config.service_name service_app_name = config.service_name attributes = {SERVICE_NAME: service_name, HOST_NAME: socket.gethostname()} - if not token: - logger.error("token not provided in trace config.") - raise ValueError("token must be provided!") - attributes["service.app.name"] = service_app_name # ToDo: change to adaptive versioning @@ -73,19 +89,31 @@ def init_instrument(config: TraceConfig): global exporter if exporter is None: - exporter = ReloadableOTLPSpanExporter( - endpoint=grpc_endpoint, headers=(f"Authentication={token}") - ) + if config.exporter_type == "grpc": + logger.info(f"Use grpc exporter: {trace_endpoint}") + exporter = ReloadableGrpcOTLPSpanExporter( + endpoint=trace_endpoint, headers=(f"Authentication={token}") + ) + elif config.exporter_type == "http": + logger.info(f"Use http exporter: {trace_endpoint}") + exporter = ReloadableHttpOTLPSpanExporter( + endpoint=trace_endpoint, headers=(f"Authentication={token}") + ) + else: + raise ValueError(f"Invalid exporter type: {config.exporter_type}") else: - exporter.reload(endpoint=grpc_endpoint, headers=(f"Authentication={token}")) + exporter.reload(endpoint=trace_endpoint, headers=(f"Authentication={token}")) global trace_provider if trace_provider is None: span_processor = BatchSpanProcessor(exporter) trace_provider = TracerProvider( - resource=resource, active_span_processor=span_processor + resource=resource ) - + trace_provider.add_span_processor(span_processor) + trace_provider.add_span_processor(LoongSuiteBaggageSpanProcessor()) + if ENABLE_TRACE_DEBUG: + trace_provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter())) trace.set_tracer_provider(trace_provider) OpenAIInstrumentor().instrument() diff --git a/backend/extensions/trace/reloadable_exporter.py b/backend/extensions/trace/grpc_exporter.py similarity index 98% rename from backend/extensions/trace/reloadable_exporter.py rename to backend/extensions/trace/grpc_exporter.py index ff7c5d769..9a793fcb0 100644 --- a/backend/extensions/trace/reloadable_exporter.py +++ b/backend/extensions/trace/grpc_exporter.py @@ -45,7 +45,7 @@ # pylint: disable=no-member -class ReloadableOTLPSpanExporter(OTLPSpanExporter): +class ReloadableGrpcOTLPSpanExporter(OTLPSpanExporter): # pylint: disable=unsubscriptable-object """OTLP span exporter diff --git a/backend/extensions/trace/http_exporter.py b/backend/extensions/trace/http_exporter.py new file mode 100644 index 000000000..7732e1225 --- /dev/null +++ b/backend/extensions/trace/http_exporter.py @@ -0,0 +1,83 @@ +# Copyright The OpenTelemetry Authors +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""OTLP Span Exporter""" + +from os import environ +from typing import Dict, Optional +from urllib.parse import urlparse +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_TRACES_ENDPOINT, + OTEL_EXPORTER_OTLP_TRACES_HEADERS, +) +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.environment_variables import ( + OTEL_EXPORTER_OTLP_HEADERS, +) +from opentelemetry.util.re import parse_env_headers + + +# pylint: disable=no-member +class ReloadableHttpOTLPSpanExporter(OTLPSpanExporter): + # pylint: disable=unsubscriptable-object + """OTLP span exporter + + Args: + endpoint: OpenTelemetry Collector receiver endpoint + insecure: Connection type + credentials: Credentials object for server authentication + headers: Headers to send when exporting + timeout: Backend request timeout in seconds + compression: gRPC compression method to use + """ + def __init__( + self, + endpoint: Optional[str] = None, + headers: Dict[str, str] = None, + timeout: Optional[int] = None, + ): + endpoint = endpoint or environ.get(OTEL_EXPORTER_OTLP_TRACES_ENDPOINT) + + assert endpoint, "endpoint is required" + endpoint = endpoint.rstrip('/') + + if not endpoint.endswith("/v1/traces"): + endpoint = f"{endpoint}/v1/traces" + + headers = headers or environ.get(OTEL_EXPORTER_OTLP_TRACES_HEADERS) + if isinstance(headers, str): + headers = parse_env_headers(headers, liberal=True) + + super().__init__( + endpoint=endpoint, + headers=headers, + timeout=timeout, + ) + + def reload( + self, + endpoint: Optional[str] = None, + headers: Dict[str, str] | str = None, + ): + self._endpoint = endpoint + + parsed_url = urlparse(self._endpoint) + + if parsed_url.netloc: + self._endpoint = parsed_url.netloc + + self._headers = headers or environ.get(OTEL_EXPORTER_OTLP_HEADERS) + if isinstance(self._headers, str): + self._headers = parse_env_headers(self._headers, liberal=True) + + self._session.headers.update(self._headers) diff --git a/backend/extensions/trace/pai_agent_wrapper.py b/backend/extensions/trace/pai_agent_wrapper.py index 121685d8d..b366c04c7 100644 --- a/backend/extensions/trace/pai_agent_wrapper.py +++ b/backend/extensions/trace/pai_agent_wrapper.py @@ -8,7 +8,6 @@ from opentelemetry.trace import set_span_in_context from opentelemetry.trace.status import Status, StatusCode from openinference.semconv.trace import SpanAttributes, OpenInferenceSpanKindValues - from extensions.trace import context as trace_context from extensions.trace.utils import pydantic_to_dict diff --git a/backend/extensions/trace/rag_wrapper.py b/backend/extensions/trace/rag_wrapper.py new file mode 100644 index 000000000..6d49272f3 --- /dev/null +++ b/backend/extensions/trace/rag_wrapper.py @@ -0,0 +1,342 @@ +from functools import wraps +import json +import os +from typing import List +from enum import Enum +from common.tool.search_result import SearchResult +from opentelemetry.trace.status import Status, StatusCode +from openinference.semconv.trace import SpanAttributes, OpenInferenceSpanKindValues + +from extensions.trace.utils import pydantic_to_dict + +from extensions.trace.tracer import get_tracer + +GEN_AI_SPAN_KIND = "gen_ai.span.kind" +GEN_AI_OPERATION_NAME = "gen_ai.operation.name" +INPUT_MESSAGES = "gen_ai.input.messages" + +INPUT_VALUE = SpanAttributes.INPUT_VALUE +OUTPUT_VALUE = SpanAttributes.OUTPUT_VALUE + +RETRIEVER_SPAN_KIND = OpenInferenceSpanKindValues.RETRIEVER.value +EMBEDDING_SPAN_KIND = OpenInferenceSpanKindValues.EMBEDDING.value +RERANKER_SPAN_KIND = OpenInferenceSpanKindValues.RERANKER.value + +RETRIEVER_OPERATION_NAME = "retrieve_documents" +EMBEDDING_OPERATION_NAME = "embedding" +RERANKER_OPERATION_NAME = "rerank_documents" + +EMBEDDING_MDOEL_NAME = "gen_ai.request.model" +EMBEDDING_DIMENSION_COUNT = "gen_ai.embeddings.dimension.count" + +RERANKER_MODEL_NAME = "gen_ai.request.model" + +# whether to disable legacy trace and only use agentscope data contract +DISABLE_LEGACY_TRACE = os.getenv("DISABLE_LEGACY_TRACE", "false").lower() in ["true", "1", "yes", "y"] + + +class RetrieverSpanNames(str, Enum): + KNOWLEDGE_RETRIEVER = "KnowledgeRetriever" + TEXT_RETRIEVER = "TextRetriever" + VECTOR_RETRIEVER = "VectorRetriever" + EMBEDDING = "Embedding" + RERANKER = "Reranker" + + +STATUS_OK = Status(StatusCode.OK) + + +def query_knowledgebase_wrapper(func): + """decorator to capture input & output string of query knowledgebase operation.""" + + @wraps(func) + async def wrapper(self, *args, **kwargs): + # if not enabled, directly return + if os.getenv("TRACING_ENABLED", "false") != "true": + return await func(self, *args, **kwargs) + + query = kwargs.get("query", "[unknown]") + messages = [{ + "role": "user", + "parts": [ + { + "type": "text", + "content": query, + } + ], + "metadata": kwargs, + }] + with get_tracer().start_as_current_span(RetrieverSpanNames.KNOWLEDGE_RETRIEVER) as span: + try: + span.set_attribute(GEN_AI_SPAN_KIND, RETRIEVER_SPAN_KIND) + span.set_attribute(INPUT_MESSAGES, json.dumps(pydantic_to_dict(messages), ensure_ascii=False)) + retrieval_setting = kwargs.get("retrieval_setting", None) + input_data = { + "query": query, + "top_k": retrieval_setting.top_k if retrieval_setting else None, + "score_threshold": retrieval_setting.similarity_threshold if retrieval_setting else None, + } + span.set_attribute(INPUT_VALUE, json.dumps(input_data, ensure_ascii=False)) + + span.set_attribute(GEN_AI_OPERATION_NAME, RETRIEVER_OPERATION_NAME) + + results: List[SearchResult] = await func(self, *args, **kwargs) + output_documents = [ + { + "id": doc.id, + "content": doc.content, + "score": doc.score, + "metadata": doc.metadata, + } + for doc in results + ] + output_data = { + "documents": output_documents, + "document_size": len(output_documents), + } + output_value = json.dumps(pydantic_to_dict(output_data), ensure_ascii=False) + span.set_attribute(OUTPUT_VALUE, output_value) + span.set_status(STATUS_OK) + return results + except Exception as e: + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) + raise + + return wrapper + + +def text_search_wrapper(func): + """decorator to capture input & output string of query vector store operation.""" + + @wraps(func) + async def wrapper(*args, **kwargs): + # if not enabled, directly return + if os.getenv("TRACING_ENABLED", "false") != "true": + return await func(*args, **kwargs) + + query = kwargs.get("query", "[unknown]") + messages = [{ + "role": "user", + "parts": [ + { + "type": "text", + "content": query, + } + ], + }] + with get_tracer().start_as_current_span(RetrieverSpanNames.TEXT_RETRIEVER) as span: + try: + span.set_attribute(GEN_AI_SPAN_KIND, RETRIEVER_SPAN_KIND) + span.set_attribute(INPUT_MESSAGES, json.dumps(pydantic_to_dict(messages), ensure_ascii=False)) + input_data = { + "query": query, + "top_k": kwargs.get("top_k", None), + } + span.set_attribute(INPUT_VALUE, json.dumps(input_data, ensure_ascii=False)) + + span.set_attribute(GEN_AI_OPERATION_NAME, RETRIEVER_OPERATION_NAME) + + text_search_result = await func(*args, **kwargs) + output_documents =[] + if text_search_result and text_search_result.nodes: + output_documents = [ + { + "id": text_search_result.ids[i], + "content": text_search_result.nodes[i].text, + "score": text_search_result.similarities[i], + "metadata": text_search_result.nodes[i].metadata, + } + for i in range(len(text_search_result.nodes)) + ] + output_data = { + "documents": output_documents, + "document_size": len(output_documents), + } + output_value = json.dumps(pydantic_to_dict(output_data), ensure_ascii=False) + span.set_attribute(OUTPUT_VALUE, output_value) + span.set_status(STATUS_OK) + return text_search_result + except Exception as e: + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) + raise + + return wrapper + + +def vector_search_wrapper(func): + """decorator to capture input & output string of query vector store operation.""" + + @wraps(func) + async def wrapper(*args, **kwargs): + # if not enabled, directly return + if os.getenv("TRACING_ENABLED", "false") != "true": + return await func(*args, **kwargs) + + query = kwargs.get("query", "[unknown]") + messages = [{ + "role": "user", + "parts": [ + { + "type": "text", + "content": query, + } + ], + }] + with get_tracer().start_as_current_span(RetrieverSpanNames.VECTOR_RETRIEVER) as span: + try: + span.set_attribute(GEN_AI_SPAN_KIND, RETRIEVER_SPAN_KIND) + span.set_attribute(INPUT_MESSAGES, json.dumps(pydantic_to_dict(messages), ensure_ascii=False)) + input_data = { + "query": query, + "top_k": kwargs.get("top_k", None), + } + span.set_attribute(INPUT_VALUE, json.dumps(input_data, ensure_ascii=False)) + span.set_attribute(GEN_AI_OPERATION_NAME, RETRIEVER_OPERATION_NAME) + + vector_search_result = await func(*args, **kwargs) + output_documents =[] + if vector_search_result and vector_search_result.nodes: + output_documents = [ + { + "id": vector_search_result.ids[i], + "content": vector_search_result.nodes[i].text, + "score": vector_search_result.similarities[i], + "metadata": vector_search_result.nodes[i].metadata, + } + for i in range(len(vector_search_result.nodes)) + ] + output_data = { + "documents": output_documents, + "document_size": len(output_documents), + } + output_value = json.dumps(pydantic_to_dict(output_data), ensure_ascii=False) + span.set_attribute(OUTPUT_VALUE, output_value) + span.set_status(STATUS_OK) + return vector_search_result + except Exception as e: + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) + raise + + return wrapper + + +def embedding_wrapper(func): + """decorator to capture input & output string of query knowledgebase operation.""" + + @wraps(func) + async def wrapper(self, *args, **kwargs): + # if not enabled, directly return + if os.getenv("TRACING_ENABLED", "false") != "true": + return await func(self, *args, **kwargs) + + query = kwargs.get("query", "[unknown]") + embedding_model_entity = kwargs.get("embedding_model_entity", None) + messages = [{ + "role": "user", + "parts": [ + { + "type": "text", + "content": query, + } + ], + }] + try: + span = get_tracer().start_span(RetrieverSpanNames.EMBEDDING) + span.set_attribute(GEN_AI_SPAN_KIND, EMBEDDING_SPAN_KIND) + span.set_attribute(INPUT_MESSAGES, json.dumps(pydantic_to_dict(messages), ensure_ascii=False)) + span.set_attribute(EMBEDDING_MDOEL_NAME, embedding_model_entity.model_name) + span.set_attribute(INPUT_VALUE, query) + + span.set_attribute(GEN_AI_OPERATION_NAME, EMBEDDING_OPERATION_NAME) + + query_embedding = await func(self, *args, **kwargs) + span.set_attribute(EMBEDDING_DIMENSION_COUNT, len(query_embedding)) + span.set_status(STATUS_OK) + span.end() + return query_embedding + except Exception as e: + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) + span.end() + raise + + return wrapper + + +def reranker_wrapper(func): + """decorator to capture input & output string of reranker operation.""" + + @wraps(func) + async def wrapper(self, *args, **kwargs): + # if not enabled, directly return + if os.getenv("TRACING_ENABLED", "false") != "true": + return await func(self, *args, **kwargs) + + query = kwargs.get("query", "[unknown]") + vector_result = kwargs.get("vector_result", None) + top_n = kwargs.get("top_n", None) + try: + span = get_tracer().start_span(RetrieverSpanNames.RERANKER) + span.set_attribute(GEN_AI_SPAN_KIND, RERANKER_SPAN_KIND) + span.set_attribute(RERANKER_MODEL_NAME, self.model) + messages = [{ + "role": "user", + "parts": [ + { + "type": "text", + "content": query, + } + ], + }] + span.set_attribute(INPUT_MESSAGES, json.dumps(pydantic_to_dict(messages), ensure_ascii=False)) + + if vector_result and vector_result.nodes: + input_documents = [ + { + "id": vector_result.ids[i], + "content": vector_result.nodes[i].text, + "score": vector_result.similarities[i], + "metadata": vector_result.nodes[i].metadata, + } + for i in range(len(vector_result.nodes)) + ] + else: + input_documents = [] + + input_value = { + "documents": input_documents, + "query": query, + "document_size": len(input_documents), + "top_k": top_n, + } + span.set_attribute(INPUT_VALUE, json.dumps(pydantic_to_dict(input_value), ensure_ascii=False)) + + rerank_result = await func(self, *args, **kwargs) + output_documents = [] + if rerank_result and rerank_result.nodes: + output_documents = [ + { + "id": rerank_result.ids[i], + "content": rerank_result.nodes[i].text, + "score": rerank_result.similarities[i], + "metadata": rerank_result.nodes[i].metadata, + } + for i in range(len(rerank_result.nodes)) + ] + output_value = { + "documents": output_documents + } + span.set_attribute(OUTPUT_VALUE, json.dumps(pydantic_to_dict(output_value), ensure_ascii=False)) + span.set_status(STATUS_OK) + span.end() + return rerank_result + except Exception as e: + span.record_exception(e) + span.set_status(Status(StatusCode.ERROR, str(e))) + span.end() + raise + + return wrapper diff --git a/backend/extensions/trace/trace_config.py b/backend/extensions/trace/trace_config.py index 74d2d3c20..821c0c7e3 100644 --- a/backend/extensions/trace/trace_config.py +++ b/backend/extensions/trace/trace_config.py @@ -2,6 +2,7 @@ class TraceConfig(BaseModel): + exporter_type: str = "grpc" service_name: str | None = None token: str | None = None endpoint: str | None = None @@ -11,4 +12,4 @@ class TraceConfig(BaseModel): user_args: dict | None = None def is_enabled(self) -> bool: - return self.enabled and self.service_name and self.token and self.endpoint + return self.enabled and self.service_name and self.endpoint diff --git a/backend/extensions/trace/trace_context_middleware.py b/backend/extensions/trace/trace_context_middleware.py new file mode 100644 index 000000000..9a58deed1 --- /dev/null +++ b/backend/extensions/trace/trace_context_middleware.py @@ -0,0 +1,25 @@ +# backend/middleware/trace_context_advanced.py + +from fastapi import Request +from starlette.middleware.base import BaseHTTPMiddleware +from opentelemetry.propagate import get_global_textmap +from opentelemetry import context + +class TraceContextMiddleware(BaseHTTPMiddleware): + """ + 提取 Trace Context 并设置到日志上下文 + """ + + async def dispatch(self, request: Request, call_next): + # 提取 trace context + carrier = dict(request.headers) + propagator = get_global_textmap() + extracted_context = propagator.extract(carrier=carrier) + + # 在提取的 context 中执行 + token = context.attach(extracted_context) + try: + response = await call_next(request) + return response + finally: + context.detach(token) diff --git a/backend/extensions/trace/tracer.py b/backend/extensions/trace/tracer.py index 17c5f6473..616bb6f96 100644 --- a/backend/extensions/trace/tracer.py +++ b/backend/extensions/trace/tracer.py @@ -14,7 +14,6 @@ def start_span(self, name, ctx=None, *args, **kwargs): def start_as_current_span(self, *args, **kwargs): return self._tracer.start_as_current_span(*args, **kwargs) - def get_tracer(): tracer = trace.get_tracer(__name__) return ContextAwareTracer(tracer) diff --git a/backend/rag/rerank/dashscope_reranker.py b/backend/rag/rerank/dashscope_reranker.py index c23ac6588..3acfda4c5 100644 --- a/backend/rag/rerank/dashscope_reranker.py +++ b/backend/rag/rerank/dashscope_reranker.py @@ -4,6 +4,7 @@ import aiohttp from utils.http_session import HttpSessionShared from rag.rerank.reranker import RerankResult +from extensions.trace.rag_wrapper import reranker_wrapper from loguru import logger @@ -177,10 +178,11 @@ async def rerank( except json.JSONDecodeError as e: raise RuntimeError(f"响应解析失败: {str(e)}") from e + @reranker_wrapper async def vector_store_rerank( self, query: str, - result: VectorStoreQueryResult, + vector_result: VectorStoreQueryResult, top_n: Optional[int] = None, similarity_threshold: float = 0, model: Optional[str] = None, @@ -204,10 +206,13 @@ async def vector_store_rerank( # 参数验证 if not query: raise ValueError("查询内容不能为空") - if not result: + if not vector_result: raise ValueError("VectorStoreQueryResult列表不能为空") - origin_nodes = result.nodes + if not vector_result.nodes or len(vector_result.nodes) <= 1: + return vector_result + + origin_nodes = vector_result.nodes documents = [node.text for node in origin_nodes] rerank_results = await self.rerank(query, documents, model, top_n, similarity_threshold) diff --git a/backend/rag/rerank/fusion_reranker.py b/backend/rag/rerank/fusion_reranker.py index ce4b57f4f..b31772414 100644 --- a/backend/rag/rerank/fusion_reranker.py +++ b/backend/rag/rerank/fusion_reranker.py @@ -165,7 +165,7 @@ async def arerank_fusion( else: return await rerank_model.vector_store_rerank( query=query, - result=dense_result, + vector_result=dense_result, top_n=rerank_top_k, similarity_threshold=similarity_threshold) elif not dense_result: @@ -174,7 +174,7 @@ async def arerank_fusion( else: return await rerank_model.vector_store_rerank( query=query, - result=text_result, + vector_result=text_result, top_n=rerank_top_k, similarity_threshold=similarity_threshold) else: @@ -184,6 +184,6 @@ async def arerank_fusion( merged_result = merge_vector_store_results_by_text(text_result, dense_result) return await rerank_model.vector_store_rerank( query=query, - result=merged_result, + vector_result=merged_result, top_n=rerank_top_k, similarity_threshold=similarity_threshold) diff --git a/backend/rag/rerank/reranker.py b/backend/rag/rerank/reranker.py index 875e9d742..202153190 100644 --- a/backend/rag/rerank/reranker.py +++ b/backend/rag/rerank/reranker.py @@ -4,6 +4,7 @@ from llama_index.core.vector_stores.types import VectorStoreQueryResult import aiohttp from utils.http_session import HttpSessionShared +from extensions.trace.rag_wrapper import reranker_wrapper @dataclass @@ -186,10 +187,11 @@ async def rerank( except json.JSONDecodeError as e: raise RuntimeError(f"响应解析失败: {str(e)}") from e + @reranker_wrapper async def vector_store_rerank( self, query: str, - result: VectorStoreQueryResult, + vector_result: VectorStoreQueryResult, top_n: Optional[int] = None, model: Optional[str] = None, similarity_threshold: float = 0, @@ -199,7 +201,7 @@ async def vector_store_rerank( Args: query: 查询语句 - result: 需要排序的vector store query result + vector_result: 需要排序的vector store query result top_n: 返回的最相关node数量 model: 覆盖默认模型 similarity_threshold: 相似度阈值 @@ -214,10 +216,13 @@ async def vector_store_rerank( # 参数验证 if not query: raise ValueError("查询内容不能为空") - if not result: + if not vector_result: raise ValueError("VectorStoreQueryResult列表不能为空") - origin_nodes = result.nodes + if not vector_result.nodes or len(vector_result.nodes) <= 1: + return vector_result + + origin_nodes = vector_result.nodes documents=[node.text for node in origin_nodes] rerank_results = await self.rerank(query, documents, model, top_n, similarity_threshold) diff --git a/backend/service/knowledgebase/rag_service.py b/backend/service/knowledgebase/rag_service.py index 35a4716d3..22c1bc38b 100644 --- a/backend/service/knowledgebase/rag_service.py +++ b/backend/service/knowledgebase/rag_service.py @@ -19,6 +19,7 @@ KbMetadataEntity, KbMetadataEntityCreate, ) +from db.models.knowledgebase.embedding import EmbeddingModelEntity from db.models.knowledgebase.chunk import KbChunkEntity, create_text_node_from_chunk from llama_index.core.vector_stores.types import VectorStoreQueryResult from service.knowledgebase.utils.metadata_utils import validate_metadata_value @@ -38,6 +39,7 @@ from utils.lru_cache import LruCache from db.db_context import create_db_session from common.knowledgebase.constants import DEFAULT_VECTOR_WEIGHT, DEFAULT_SIMILARITY_TOP_K, DEFAULT_RERANK_SIMILARITY_TOP_K +from extensions.trace.rag_wrapper import query_knowledgebase_wrapper, embedding_wrapper from loguru import logger MARKDOWN_IMAGE_PATTERN = r'!\[.*?\]\((.*?)\)\s*\n*\s*图片的描述:\s*(.*?)(?=\n\n|$)' @@ -816,6 +818,7 @@ async def format_search_result(self, reranked_result: VectorStoreQueryResult, te records.append( SearchResult( + id=reranked_result.ids[i], score=reranked_result.similarities[i], content=origin_text[:3000], images=images, @@ -826,8 +829,14 @@ async def format_search_result(self, reranked_result: VectorStoreQueryResult, te return records + @embedding_wrapper + async def embed_query(self, query: str, embedding_model_entity: EmbeddingModelEntity) -> List[float]: + embed_model = create_embedding_model(embedding_model_entity) + query_embedding = await embed_model.aget_query_embedding(query) + return query_embedding + # 当需要发起SessionScope并发时,每个查询都需要独立的session实例 - async def _aquery_vector_store( + async def _aquery_task( self, session: AsyncSession, kb_id: str = None, @@ -863,8 +872,7 @@ async def _aquery_vector_store( if not embed_model_entity: raise ValueError(f"Embedding model not found for knowledgebase {kb_id}.") - embed_model = create_embedding_model(embed_model_entity) - query_embedding = await embed_model.aget_query_embedding(query) + query_embedding = await self.embed_query(query=query, embedding_model_entity=embed_model_entity) if not retrieval_setting: retrieval_setting = RetrievalSetting() @@ -976,7 +984,7 @@ async def aquery_task( ): if session is None: async with create_db_session() as new_session: - return await self._aquery_vector_store( + return await self._aquery_task( session=new_session, kb_id=kb_id, kb_name=kb_name, @@ -988,7 +996,7 @@ async def aquery_task( tenant_id=tenant_id, ) else: - return await self._aquery_vector_store( + return await self._aquery_task( session=session, kb_id=kb_id, kb_name=kb_name, @@ -1001,6 +1009,7 @@ async def aquery_task( ) ## VectorDB Retrieval Service + @query_knowledgebase_wrapper async def aquery( self, query: str, diff --git a/backend/service/tool/trace_service.py b/backend/service/tool/trace_service.py index 9c2400067..edd445f30 100644 --- a/backend/service/tool/trace_service.py +++ b/backend/service/tool/trace_service.py @@ -5,9 +5,29 @@ from sqlmodel.ext.asyncio.session import AsyncSession from sqlalchemy.exc import IntegrityError from loguru import logger - +import os from db.models.trace import TraceModel, TraceModelEntity from extensions.trace.base import init_instrument, TraceConfig +from common.system_constants import DEFAULT_TENANT_ID + + +DEFAULT_SERVICE_NAME = "pai-rag" + +def _load_trace_config_from_env() -> TraceModelEntity: + logger.info("Loading trace config from environment variables.") + trace_endpoint = os.getenv("TRACE_ENDPOINT") + if trace_endpoint: + logger.info(f"Trace endpoint: {trace_endpoint}") + return TraceModelEntity.model_validate({ + "endpoint": trace_endpoint, + "token": os.getenv("TRACE_TOKEN",""), + "service_name": os.getenv("TRACE_SERVICE_NAME", DEFAULT_SERVICE_NAME), + "tenant_id": DEFAULT_TENANT_ID, + "id": "default_trace_id", + "enabled": True + }) + else: + return None class TraceService: @@ -23,9 +43,13 @@ def __init__(self, session: AsyncSession): self.session = session async def init_trace(self): - statement = select(TraceModelEntity).where(TraceModelEntity.enabled) - result = await self.session.exec(statement) - config = result.first() + exporter_type = os.getenv("TRACE_EXPORTER_TYPE", "grpc") + config = _load_trace_config_from_env() + if not config: + statement = select(TraceModelEntity).where(TraceModelEntity.enabled) + result = await self.session.exec(statement) + config = result.first() + if config and config.is_enabled(): init_instrument(TraceConfig( endpoint=config.endpoint, @@ -33,6 +57,7 @@ async def init_trace(self): service_name=config.service_name, user_args=config.user_args, enabled=config.enabled, + exporter_type=exporter_type, )) logger.info("Initialized trace config.") else: @@ -103,6 +128,7 @@ async def create_or_update_trace_config( service_name=config.service_name, user_args=config.user_args, enabled=config.enabled, + exporter_type="grpc", # 默认grpc )) return config diff --git a/backend/tools/utils/vectordb_retrieval.py b/backend/tools/utils/vectordb_retrieval.py index 5fd509c75..de24ae457 100644 --- a/backend/tools/utils/vectordb_retrieval.py +++ b/backend/tools/utils/vectordb_retrieval.py @@ -6,7 +6,7 @@ from loguru import logger import asyncio from rag.rerank.fusion_reranker import min_max_normalize_scores - +from extensions.trace.rag_wrapper import text_search_wrapper, vector_search_wrapper def retrieval_type_to_search_mode(retrieval_type: VectorIndexRetrievalType): if retrieval_type == VectorIndexRetrievalType.fulltext: @@ -17,6 +17,48 @@ def retrieval_type_to_search_mode(retrieval_type: VectorIndexRetrievalType): return VectorStoreQueryMode.DEFAULT +@text_search_wrapper +async def _aquery_text( + vector_store: BasePydanticVectorStore, + query: str, + document_ids: List[str], + top_k: int, + metadata_filters: Optional[MetadataFilters] = None, +) -> VectorStoreQueryResult: + query_kwargs = { + "query_str": query, + "similarity_top_k": top_k, + "mode": VectorStoreQueryMode.TEXT_SEARCH, + } + if metadata_filters: + query_kwargs["filters"] = metadata_filters + elif document_ids: + query_kwargs["doc_ids"] = document_ids + return await vector_store.aquery(VectorStoreQuery(**query_kwargs)) + + +@vector_search_wrapper +async def _aquery_vector( + vector_store: BasePydanticVectorStore, + query: str, + query_embedding: List[float], + document_ids: List[str], + top_k: int, + metadata_filters: Optional[MetadataFilters] = None, +) -> VectorStoreQueryResult: + query_kwargs = { + "query_embedding": query_embedding, + "similarity_top_k": top_k, + "mode": VectorStoreQueryMode.DEFAULT, + "query_str": query, + } + if metadata_filters: + query_kwargs["filters"] = metadata_filters + elif document_ids: + query_kwargs["doc_ids"] = document_ids + return await vector_store.aquery(VectorStoreQuery(**query_kwargs)) + + async def aquery_vector_store( vector_store: BasePydanticVectorStore, query: str, @@ -50,45 +92,51 @@ async def aquery_vector_store( else: logger.info("Using doc_id as filters.") - def _build_query_kwargs(mode: VectorStoreQueryMode): - kwargs = { - "query_embedding": query_embedding, - "similarity_top_k": top_k, - "query_str": query, - "mode": mode, - } - if use_docid_filter: - kwargs["doc_ids"] = document_ids - elif metadata_filters: - kwargs["filters"] = metadata_filters - return kwargs try: if query_mode == VectorStoreQueryMode.HYBRID: # 混合模式:并行执行文本搜索和向量搜索 - text_query = VectorStoreQuery(**_build_query_kwargs(VectorStoreQueryMode.TEXT_SEARCH)) - dense_query = VectorStoreQuery(**_build_query_kwargs(VectorStoreQueryMode.DEFAULT)) - - text_result_task = vector_store.aquery(text_query) - dense_result_task = vector_store.aquery(dense_query) + text_result_task = _aquery_text( + vector_store=vector_store, + query=query, + document_ids=document_ids, + top_k=top_k, + metadata_filters=metadata_filters, + ) + dense_result_task = _aquery_vector( + vector_store=vector_store, + query=query, + query_embedding=query_embedding, + document_ids=document_ids, + top_k=top_k, + metadata_filters=metadata_filters, + ) text_result, dense_result = await asyncio.gather(text_result_task, dense_result_task) logger.info(f"HYBRID mode: Retrieved {len(text_result.nodes)} text nodes and {len(dense_result.nodes)} dense nodes.") + elif query_mode == VectorStoreQueryMode.TEXT_SEARCH: + text_result = await _aquery_text( + vector_store=vector_store, + query=query, + document_ids=document_ids, + top_k=top_k, + metadata_filters=metadata_filters, + ) + logger.info(f"{query_mode} mode: Retrieved {len(text_result.nodes)} nodes.") else: - vector_query = VectorStoreQuery(**_build_query_kwargs(query_mode)) - query_result = await vector_store.aquery(vector_query) - - if query_mode == VectorStoreQueryMode.TEXT_SEARCH: - text_result = query_result - else: - dense_result = query_result - - logger.info(f"{query_mode} mode: Retrieved {len(query_result.nodes)} nodes.") + dense_result = await _aquery_vector( + vector_store=vector_store, + query=query, + query_embedding=query_embedding, + document_ids=document_ids, + top_k=top_k, + metadata_filters=metadata_filters, + ) + logger.info(f"{query_mode} mode: Retrieved {len(dense_result.nodes)} nodes.") # TEXT_SEARCH 模式的分数归一化 if text_result and text_result.similarities: text_result.similarities = min_max_normalize_scores(text_result.similarities) - except Exception as e: logger.error(f"Failed to query vector store: {e}") raise diff --git a/frontend/app/config/tracing/page.tsx b/frontend/app/config/tracing/page.tsx index b2ea4d652..4b811d6e7 100644 --- a/frontend/app/config/tracing/page.tsx +++ b/frontend/app/config/tracing/page.tsx @@ -32,7 +32,7 @@ export default function TracingConfig() { if (!res.ok) throw new Error('加载配置失败'); - const data = await res.json(); + const data = (await res.json()).data; setEndpoint(data['endpoint'] || ''); setToken(data['token'] || ''); setServiceName(data['service_name'] || ''); diff --git a/poetry.lock b/poetry.lock index 0f2cd4fd8..62924751b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -791,6 +791,21 @@ starlette = ">=0.18" [package.extras] celery = ["celery"] +[[package]] +name = "asgiref" +version = "3.11.0" +description = "ASGI specs, helper code, and adapters" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "asgiref-3.11.0-py3-none-any.whl", hash = "sha256:1db9021efadb0d9512ce8ffaf72fcef601c7b73a8807a1bb2ef143dc6b14846d"}, + {file = "asgiref-3.11.0.tar.gz", hash = "sha256:13acff32519542a1736223fb79a715acdebe24286d98e8b164a73085f40da2c4"}, +] + +[package.extras] +tests = ["mypy (>=1.14.0)", "pytest", "pytest-asyncio"] + [[package]] name = "async-timeout" version = "5.0.1" @@ -5227,14 +5242,14 @@ et-xmlfile = "*" [[package]] name = "opentelemetry-api" -version = "1.38.0" +version = "1.39.1" description = "OpenTelemetry Python API" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "opentelemetry_api-1.38.0-py3-none-any.whl", hash = "sha256:2891b0197f47124454ab9f0cf58f3be33faca394457ac3e09daba13ff50aa582"}, - {file = "opentelemetry_api-1.38.0.tar.gz", hash = "sha256:f4c193b5e8acb0912b06ac5b16321908dd0843d75049c091487322284a3eea12"}, + {file = "opentelemetry_api-1.39.1-py3-none-any.whl", hash = "sha256:2edd8463432a7f8443edce90972169b195e7d6a05500cd29e6d13898187c9950"}, + {file = "opentelemetry_api-1.39.1.tar.gz", hash = "sha256:fbde8c80e1b937a2c61f20347e91c0c18a1940cecf012d62e65a7caf08967c9c"}, ] [package.dependencies] @@ -5243,68 +5258,139 @@ typing-extensions = ">=4.5.0" [[package]] name = "opentelemetry-exporter-otlp-proto-common" -version = "1.38.0" +version = "1.39.1" description = "OpenTelemetry Protobuf encoding" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a"}, - {file = "opentelemetry_exporter_otlp_proto_common-1.38.0.tar.gz", hash = "sha256:e333278afab4695aa8114eeb7bf4e44e65c6607d54968271a249c180b2cb605c"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.39.1-py3-none-any.whl", hash = "sha256:08f8a5862d64cc3435105686d0216c1365dc5701f86844a8cd56597d0c764fde"}, + {file = "opentelemetry_exporter_otlp_proto_common-1.39.1.tar.gz", hash = "sha256:763370d4737a59741c89a67b50f9e39271639ee4afc999dadfe768541c027464"}, ] [package.dependencies] -opentelemetry-proto = "1.38.0" +opentelemetry-proto = "1.39.1" [[package]] name = "opentelemetry-exporter-otlp-proto-grpc" -version = "1.38.0" +version = "1.39.1" description = "OpenTelemetry Collector Protobuf over gRPC Exporter" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "opentelemetry_exporter_otlp_proto_grpc-1.38.0-py3-none-any.whl", hash = "sha256:7c49fd9b4bd0dbe9ba13d91f764c2d20b0025649a6e4ac35792fb8d84d764bc7"}, - {file = "opentelemetry_exporter_otlp_proto_grpc-1.38.0.tar.gz", hash = "sha256:2473935e9eac71f401de6101d37d6f3f0f1831db92b953c7dcc912536158ebd6"}, + {file = "opentelemetry_exporter_otlp_proto_grpc-1.39.1-py3-none-any.whl", hash = "sha256:fa1c136a05c7e9b4c09f739469cbdb927ea20b34088ab1d959a849b5cc589c18"}, + {file = "opentelemetry_exporter_otlp_proto_grpc-1.39.1.tar.gz", hash = "sha256:772eb1c9287485d625e4dbe9c879898e5253fea111d9181140f51291b5fec3ad"}, ] [package.dependencies] googleapis-common-protos = ">=1.57,<2.0" grpcio = {version = ">=1.63.2,<2.0.0", markers = "python_version < \"3.13\""} opentelemetry-api = ">=1.15,<2.0" -opentelemetry-exporter-otlp-proto-common = "1.38.0" -opentelemetry-proto = "1.38.0" -opentelemetry-sdk = ">=1.38.0,<1.39.0" +opentelemetry-exporter-otlp-proto-common = "1.39.1" +opentelemetry-proto = "1.39.1" +opentelemetry-sdk = ">=1.39.1,<1.40.0" typing-extensions = ">=4.6.0" +[package.extras] +gcp-auth = ["opentelemetry-exporter-credential-provider-gcp (>=0.59b0)"] + +[[package]] +name = "opentelemetry-exporter-otlp-proto-http" +version = "1.39.1" +description = "OpenTelemetry Collector Protobuf over HTTP Exporter" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "opentelemetry_exporter_otlp_proto_http-1.39.1-py3-none-any.whl", hash = "sha256:d9f5207183dd752a412c4cd564ca8875ececba13be6e9c6c370ffb752fd59985"}, + {file = "opentelemetry_exporter_otlp_proto_http-1.39.1.tar.gz", hash = "sha256:31bdab9745c709ce90a49a0624c2bd445d31a28ba34275951a6a362d16a0b9cb"}, +] + +[package.dependencies] +googleapis-common-protos = ">=1.52,<2.0" +opentelemetry-api = ">=1.15,<2.0" +opentelemetry-exporter-otlp-proto-common = "1.39.1" +opentelemetry-proto = "1.39.1" +opentelemetry-sdk = ">=1.39.1,<1.40.0" +requests = ">=2.7,<3.0" +typing-extensions = ">=4.5.0" + +[package.extras] +gcp-auth = ["opentelemetry-exporter-credential-provider-gcp (>=0.59b0)"] + [[package]] name = "opentelemetry-instrumentation" -version = "0.59b0" +version = "0.60b1" description = "Instrumentation Tools & Auto Instrumentation for OpenTelemetry Python" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "opentelemetry_instrumentation-0.59b0-py3-none-any.whl", hash = "sha256:44082cc8fe56b0186e87ee8f7c17c327c4c2ce93bdbe86496e600985d74368ee"}, - {file = "opentelemetry_instrumentation-0.59b0.tar.gz", hash = "sha256:6010f0faaacdaf7c4dff8aac84e226d23437b331dcda7e70367f6d73a7db1adc"}, + {file = "opentelemetry_instrumentation-0.60b1-py3-none-any.whl", hash = "sha256:04480db952b48fb1ed0073f822f0ee26012b7be7c3eac1a3793122737c78632d"}, + {file = "opentelemetry_instrumentation-0.60b1.tar.gz", hash = "sha256:57ddc7974c6eb35865af0426d1a17132b88b2ed8586897fee187fd5b8944bd6a"}, ] [package.dependencies] opentelemetry-api = ">=1.4,<2.0" -opentelemetry-semantic-conventions = "0.59b0" +opentelemetry-semantic-conventions = "0.60b1" packaging = ">=18.0" wrapt = ">=1.0.0,<2.0.0" +[[package]] +name = "opentelemetry-instrumentation-asgi" +version = "0.60b1" +description = "ASGI instrumentation for OpenTelemetry" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "opentelemetry_instrumentation_asgi-0.60b1-py3-none-any.whl", hash = "sha256:d48def2dbed10294c99cfcf41ebbd0c414d390a11773a41f472d20000fcddc25"}, + {file = "opentelemetry_instrumentation_asgi-0.60b1.tar.gz", hash = "sha256:16bfbe595cd24cda309a957456d0fc2523f41bc7b076d1f2d7e98a1ad9876d6f"}, +] + +[package.dependencies] +asgiref = ">=3.0,<4.0" +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.60b1" +opentelemetry-semantic-conventions = "0.60b1" +opentelemetry-util-http = "0.60b1" + +[package.extras] +instruments = ["asgiref (>=3.0,<4.0)"] + +[[package]] +name = "opentelemetry-instrumentation-fastapi" +version = "0.60b1" +description = "OpenTelemetry FastAPI Instrumentation" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "opentelemetry_instrumentation_fastapi-0.60b1-py3-none-any.whl", hash = "sha256:af94b7a239ad1085fc3a820ecf069f67f579d7faf4c085aaa7bd9b64eafc8eaf"}, + {file = "opentelemetry_instrumentation_fastapi-0.60b1.tar.gz", hash = "sha256:de608955f7ff8eecf35d056578346a5365015fd7d8623df9b1f08d1c74769c01"}, +] + +[package.dependencies] +opentelemetry-api = ">=1.12,<2.0" +opentelemetry-instrumentation = "0.60b1" +opentelemetry-instrumentation-asgi = "0.60b1" +opentelemetry-semantic-conventions = "0.60b1" +opentelemetry-util-http = "0.60b1" + +[package.extras] +instruments = ["fastapi (>=0.92,<1.0)"] + [[package]] name = "opentelemetry-proto" -version = "1.38.0" +version = "1.39.1" description = "OpenTelemetry Python Proto" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "opentelemetry_proto-1.38.0-py3-none-any.whl", hash = "sha256:b6ebe54d3217c42e45462e2a1ae28c3e2bf2ec5a5645236a490f55f45f1a0a18"}, - {file = "opentelemetry_proto-1.38.0.tar.gz", hash = "sha256:88b161e89d9d372ce723da289b7da74c3a8354a8e5359992be813942969ed468"}, + {file = "opentelemetry_proto-1.39.1-py3-none-any.whl", hash = "sha256:22cdc78efd3b3765d09e68bfbd010d4fc254c9818afd0b6b423387d9dee46007"}, + {file = "opentelemetry_proto-1.39.1.tar.gz", hash = "sha256:6c8e05144fc0d3ed4d22c2289c6b126e03bcd0e6a7da0f16cedd2e1c2772e2c8"}, ] [package.dependencies] @@ -5312,37 +5398,49 @@ protobuf = ">=5.0,<7.0" [[package]] name = "opentelemetry-sdk" -version = "1.38.0" +version = "1.39.1" description = "OpenTelemetry Python SDK" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "opentelemetry_sdk-1.38.0-py3-none-any.whl", hash = "sha256:1c66af6564ecc1553d72d811a01df063ff097cdc82ce188da9951f93b8d10f6b"}, - {file = "opentelemetry_sdk-1.38.0.tar.gz", hash = "sha256:93df5d4d871ed09cb4272305be4d996236eedb232253e3ab864c8620f051cebe"}, + {file = "opentelemetry_sdk-1.39.1-py3-none-any.whl", hash = "sha256:4d5482c478513ecb0a5d938dcc61394e647066e0cc2676bee9f3af3f3f45f01c"}, + {file = "opentelemetry_sdk-1.39.1.tar.gz", hash = "sha256:cf4d4563caf7bff906c9f7967e2be22d0d6b349b908be0d90fb21c8e9c995cc6"}, ] [package.dependencies] -opentelemetry-api = "1.38.0" -opentelemetry-semantic-conventions = "0.59b0" +opentelemetry-api = "1.39.1" +opentelemetry-semantic-conventions = "0.60b1" typing-extensions = ">=4.5.0" [[package]] name = "opentelemetry-semantic-conventions" -version = "0.59b0" +version = "0.60b1" description = "OpenTelemetry Semantic Conventions" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed"}, - {file = "opentelemetry_semantic_conventions-0.59b0.tar.gz", hash = "sha256:7a6db3f30d70202d5bf9fa4b69bc866ca6a30437287de6c510fb594878aed6b0"}, + {file = "opentelemetry_semantic_conventions-0.60b1-py3-none-any.whl", hash = "sha256:9fa8c8b0c110da289809292b0591220d3a7b53c1526a23021e977d68597893fb"}, + {file = "opentelemetry_semantic_conventions-0.60b1.tar.gz", hash = "sha256:87c228b5a0669b748c76d76df6c364c369c28f1c465e50f661e39737e84bc953"}, ] [package.dependencies] -opentelemetry-api = "1.38.0" +opentelemetry-api = "1.39.1" typing-extensions = ">=4.5.0" +[[package]] +name = "opentelemetry-util-http" +version = "0.60b1" +description = "Web util for OpenTelemetry" +optional = false +python-versions = ">=3.9" +groups = ["main"] +files = [ + {file = "opentelemetry_util_http-0.60b1-py3-none-any.whl", hash = "sha256:66381ba28550c91bee14dcba8979ace443444af1ed609226634596b4b0faf199"}, + {file = "opentelemetry_util_http-0.60b1.tar.gz", hash = "sha256:0d97152ca8c8a41ced7172d29d3622a219317f74ae6bb3027cfbdcf22c3cc0d6"}, +] + [[package]] name = "orjson" version = "3.11.4" @@ -9608,4 +9706,4 @@ files = [ [metadata] lock-version = "2.1" python-versions = ">=3.11.0,<3.13" -content-hash = "a959d1bc54d261265e104d6ad93fdf598a0a747135c01e834ac2407cb435829a" +content-hash = "d6c1e7280499d157a8ea5df1b29b75c046fb023cdab8e8857ce40ab730bfad7b" diff --git a/pyproject.toml b/pyproject.toml index 6475b310a..a0b5bf931 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -79,7 +79,6 @@ alembic = "^1.16.2" celery = "^5.5.3" chromadb = "^1.3.5" redis = "^6.2.0" -opentelemetry-exporter-otlp-proto-grpc = "^1.36.0" tavily-python = "^0.7.12" rapidfuzz = "^3.14.1" xiyan-mcp-server = "^0.1.7" @@ -89,6 +88,9 @@ aiofiles = "24.1.0" alibabacloud-credentials = "1.0.1" pairag-file = {url = "https://pai-rag.oss-cn-hangzhou.aliyuncs.com/packages/python_wheels/pairag_file-2.1.23-py3-none-any.whl"} aiocache = "^0.12.3" +opentelemetry-instrumentation-fastapi = "^0.60b1" +opentelemetry-exporter-otlp-proto-http = "^1.39.1" +opentelemetry-exporter-otlp-proto-grpc = "^1.39.1" [tool.pytest.ini_options] asyncio_mode = "auto"