aliyun_trace.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  1. import json
  2. import logging
  3. from collections.abc import Sequence
  4. from typing import Optional
  5. from urllib.parse import urljoin
  6. from opentelemetry.trace import Link, Status, StatusCode
  7. from sqlalchemy import select
  8. from sqlalchemy.orm import Session, sessionmaker
  9. from core.ops.aliyun_trace.data_exporter.traceclient import (
  10. TraceClient,
  11. convert_datetime_to_nanoseconds,
  12. convert_to_span_id,
  13. convert_to_trace_id,
  14. create_link,
  15. generate_span_id,
  16. )
  17. from core.ops.aliyun_trace.entities.aliyun_trace_entity import SpanData
  18. from core.ops.aliyun_trace.entities.semconv import (
  19. GEN_AI_COMPLETION,
  20. GEN_AI_FRAMEWORK,
  21. GEN_AI_MODEL_NAME,
  22. GEN_AI_PROMPT,
  23. GEN_AI_PROMPT_TEMPLATE_TEMPLATE,
  24. GEN_AI_PROMPT_TEMPLATE_VARIABLE,
  25. GEN_AI_RESPONSE_FINISH_REASON,
  26. GEN_AI_SESSION_ID,
  27. GEN_AI_SPAN_KIND,
  28. GEN_AI_SYSTEM,
  29. GEN_AI_USAGE_INPUT_TOKENS,
  30. GEN_AI_USAGE_OUTPUT_TOKENS,
  31. GEN_AI_USAGE_TOTAL_TOKENS,
  32. GEN_AI_USER_ID,
  33. INPUT_VALUE,
  34. OUTPUT_VALUE,
  35. RETRIEVAL_DOCUMENT,
  36. RETRIEVAL_QUERY,
  37. TOOL_DESCRIPTION,
  38. TOOL_NAME,
  39. TOOL_PARAMETERS,
  40. GenAISpanKind,
  41. )
  42. from core.ops.base_trace_instance import BaseTraceInstance
  43. from core.ops.entities.config_entity import AliyunConfig
  44. from core.ops.entities.trace_entity import (
  45. BaseTraceInfo,
  46. DatasetRetrievalTraceInfo,
  47. GenerateNameTraceInfo,
  48. MessageTraceInfo,
  49. ModerationTraceInfo,
  50. SuggestedQuestionTraceInfo,
  51. ToolTraceInfo,
  52. WorkflowTraceInfo,
  53. )
  54. from core.rag.models.document import Document
  55. from core.repositories import SQLAlchemyWorkflowNodeExecutionRepository
  56. from core.workflow.entities.workflow_node_execution import (
  57. WorkflowNodeExecution,
  58. WorkflowNodeExecutionMetadataKey,
  59. WorkflowNodeExecutionStatus,
  60. )
  61. from core.workflow.nodes import NodeType
  62. from models import Account, App, EndUser, TenantAccountJoin, WorkflowNodeExecutionTriggeredFrom, db
  63. logger = logging.getLogger(__name__)
  64. class AliyunDataTrace(BaseTraceInstance):
  65. def __init__(
  66. self,
  67. aliyun_config: AliyunConfig,
  68. ):
  69. super().__init__(aliyun_config)
  70. base_url = aliyun_config.endpoint.rstrip("/")
  71. endpoint = urljoin(base_url, f"adapt_{aliyun_config.license_key}/api/otlp/traces")
  72. self.trace_client = TraceClient(service_name=aliyun_config.app_name, endpoint=endpoint)
  73. def trace(self, trace_info: BaseTraceInfo):
  74. if isinstance(trace_info, WorkflowTraceInfo):
  75. self.workflow_trace(trace_info)
  76. if isinstance(trace_info, MessageTraceInfo):
  77. self.message_trace(trace_info)
  78. if isinstance(trace_info, ModerationTraceInfo):
  79. pass
  80. if isinstance(trace_info, SuggestedQuestionTraceInfo):
  81. self.suggested_question_trace(trace_info)
  82. if isinstance(trace_info, DatasetRetrievalTraceInfo):
  83. self.dataset_retrieval_trace(trace_info)
  84. if isinstance(trace_info, ToolTraceInfo):
  85. self.tool_trace(trace_info)
  86. if isinstance(trace_info, GenerateNameTraceInfo):
  87. pass
  88. def api_check(self):
  89. return self.trace_client.api_check()
  90. def get_project_url(self):
  91. try:
  92. return self.trace_client.get_project_url()
  93. except Exception as e:
  94. logger.info("Aliyun get run url failed: %s", str(e), exc_info=True)
  95. raise ValueError(f"Aliyun get run url failed: {str(e)}")
  96. def workflow_trace(self, trace_info: WorkflowTraceInfo):
  97. trace_id = convert_to_trace_id(trace_info.workflow_run_id)
  98. links = []
  99. if trace_info.trace_id:
  100. links.append(create_link(trace_id_str=trace_info.trace_id))
  101. workflow_span_id = convert_to_span_id(trace_info.workflow_run_id, "workflow")
  102. self.add_workflow_span(trace_id, workflow_span_id, trace_info, links)
  103. workflow_node_executions = self.get_workflow_node_executions(trace_info)
  104. for node_execution in workflow_node_executions:
  105. node_span = self.build_workflow_node_span(node_execution, trace_id, trace_info, workflow_span_id)
  106. self.trace_client.add_span(node_span)
  107. def message_trace(self, trace_info: MessageTraceInfo):
  108. message_data = trace_info.message_data
  109. if message_data is None:
  110. return
  111. message_id = trace_info.message_id
  112. user_id = message_data.from_account_id
  113. if message_data.from_end_user_id:
  114. end_user_data: Optional[EndUser] = (
  115. db.session.query(EndUser).where(EndUser.id == message_data.from_end_user_id).first()
  116. )
  117. if end_user_data is not None:
  118. user_id = end_user_data.session_id
  119. status: Status = Status(StatusCode.OK)
  120. if trace_info.error:
  121. status = Status(StatusCode.ERROR, trace_info.error)
  122. trace_id = convert_to_trace_id(message_id)
  123. links = []
  124. if trace_info.trace_id:
  125. links.append(create_link(trace_id_str=trace_info.trace_id))
  126. message_span_id = convert_to_span_id(message_id, "message")
  127. message_span = SpanData(
  128. trace_id=trace_id,
  129. parent_span_id=None,
  130. span_id=message_span_id,
  131. name="message",
  132. start_time=convert_datetime_to_nanoseconds(trace_info.start_time),
  133. end_time=convert_datetime_to_nanoseconds(trace_info.end_time),
  134. attributes={
  135. GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id") or "",
  136. GEN_AI_USER_ID: str(user_id),
  137. GEN_AI_SPAN_KIND: GenAISpanKind.CHAIN.value,
  138. GEN_AI_FRAMEWORK: "dify",
  139. INPUT_VALUE: json.dumps(trace_info.inputs, ensure_ascii=False),
  140. OUTPUT_VALUE: str(trace_info.outputs),
  141. },
  142. status=status,
  143. links=links,
  144. )
  145. self.trace_client.add_span(message_span)
  146. app_model_config = getattr(trace_info.message_data, "app_model_config", {})
  147. pre_prompt = getattr(app_model_config, "pre_prompt", "")
  148. inputs_data = getattr(trace_info.message_data, "inputs", {})
  149. llm_span = SpanData(
  150. trace_id=trace_id,
  151. parent_span_id=message_span_id,
  152. span_id=convert_to_span_id(message_id, "llm"),
  153. name="llm",
  154. start_time=convert_datetime_to_nanoseconds(trace_info.start_time),
  155. end_time=convert_datetime_to_nanoseconds(trace_info.end_time),
  156. attributes={
  157. GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id") or "",
  158. GEN_AI_USER_ID: str(user_id),
  159. GEN_AI_SPAN_KIND: GenAISpanKind.LLM.value,
  160. GEN_AI_FRAMEWORK: "dify",
  161. GEN_AI_MODEL_NAME: trace_info.metadata.get("ls_model_name") or "",
  162. GEN_AI_SYSTEM: trace_info.metadata.get("ls_provider") or "",
  163. GEN_AI_USAGE_INPUT_TOKENS: str(trace_info.message_tokens),
  164. GEN_AI_USAGE_OUTPUT_TOKENS: str(trace_info.answer_tokens),
  165. GEN_AI_USAGE_TOTAL_TOKENS: str(trace_info.total_tokens),
  166. GEN_AI_PROMPT_TEMPLATE_VARIABLE: json.dumps(inputs_data, ensure_ascii=False),
  167. GEN_AI_PROMPT_TEMPLATE_TEMPLATE: pre_prompt,
  168. GEN_AI_PROMPT: json.dumps(trace_info.inputs, ensure_ascii=False),
  169. GEN_AI_COMPLETION: str(trace_info.outputs),
  170. INPUT_VALUE: json.dumps(trace_info.inputs, ensure_ascii=False),
  171. OUTPUT_VALUE: str(trace_info.outputs),
  172. },
  173. status=status,
  174. )
  175. self.trace_client.add_span(llm_span)
  176. def dataset_retrieval_trace(self, trace_info: DatasetRetrievalTraceInfo):
  177. if trace_info.message_data is None:
  178. return
  179. message_id = trace_info.message_id
  180. trace_id = convert_to_trace_id(message_id)
  181. links = []
  182. if trace_info.trace_id:
  183. links.append(create_link(trace_id_str=trace_info.trace_id))
  184. documents_data = extract_retrieval_documents(trace_info.documents)
  185. dataset_retrieval_span = SpanData(
  186. trace_id=trace_id,
  187. parent_span_id=convert_to_span_id(message_id, "message"),
  188. span_id=generate_span_id(),
  189. name="dataset_retrieval",
  190. start_time=convert_datetime_to_nanoseconds(trace_info.start_time),
  191. end_time=convert_datetime_to_nanoseconds(trace_info.end_time),
  192. attributes={
  193. GEN_AI_SPAN_KIND: GenAISpanKind.RETRIEVER.value,
  194. GEN_AI_FRAMEWORK: "dify",
  195. RETRIEVAL_QUERY: str(trace_info.inputs),
  196. RETRIEVAL_DOCUMENT: json.dumps(documents_data, ensure_ascii=False),
  197. INPUT_VALUE: str(trace_info.inputs),
  198. OUTPUT_VALUE: json.dumps(documents_data, ensure_ascii=False),
  199. },
  200. links=links,
  201. )
  202. self.trace_client.add_span(dataset_retrieval_span)
  203. def tool_trace(self, trace_info: ToolTraceInfo):
  204. if trace_info.message_data is None:
  205. return
  206. message_id = trace_info.message_id
  207. status: Status = Status(StatusCode.OK)
  208. if trace_info.error:
  209. status = Status(StatusCode.ERROR, trace_info.error)
  210. trace_id = convert_to_trace_id(message_id)
  211. links = []
  212. if trace_info.trace_id:
  213. links.append(create_link(trace_id_str=trace_info.trace_id))
  214. tool_span = SpanData(
  215. trace_id=trace_id,
  216. parent_span_id=convert_to_span_id(message_id, "message"),
  217. span_id=generate_span_id(),
  218. name=trace_info.tool_name,
  219. start_time=convert_datetime_to_nanoseconds(trace_info.start_time),
  220. end_time=convert_datetime_to_nanoseconds(trace_info.end_time),
  221. attributes={
  222. GEN_AI_SPAN_KIND: GenAISpanKind.TOOL.value,
  223. GEN_AI_FRAMEWORK: "dify",
  224. TOOL_NAME: trace_info.tool_name,
  225. TOOL_DESCRIPTION: json.dumps(trace_info.tool_config, ensure_ascii=False),
  226. TOOL_PARAMETERS: json.dumps(trace_info.tool_inputs, ensure_ascii=False),
  227. INPUT_VALUE: json.dumps(trace_info.inputs, ensure_ascii=False),
  228. OUTPUT_VALUE: str(trace_info.tool_outputs),
  229. },
  230. status=status,
  231. links=links,
  232. )
  233. self.trace_client.add_span(tool_span)
  234. def get_workflow_node_executions(self, trace_info: WorkflowTraceInfo) -> Sequence[WorkflowNodeExecution]:
  235. # through workflow_run_id get all_nodes_execution using repository
  236. session_factory = sessionmaker(bind=db.engine)
  237. # Find the app's creator account
  238. with Session(db.engine, expire_on_commit=False) as session:
  239. # Get the app to find its creator
  240. app_id = trace_info.metadata.get("app_id")
  241. if not app_id:
  242. raise ValueError("No app_id found in trace_info metadata")
  243. app_stmt = select(App).where(App.id == app_id)
  244. app = session.scalar(app_stmt)
  245. if not app:
  246. raise ValueError(f"App with id {app_id} not found")
  247. if not app.created_by:
  248. raise ValueError(f"App with id {app_id} has no creator (created_by is None)")
  249. account_stmt = select(Account).where(Account.id == app.created_by)
  250. service_account = session.scalar(account_stmt)
  251. if not service_account:
  252. raise ValueError(f"Creator account with id {app.created_by} not found for app {app_id}")
  253. current_tenant = (
  254. session.query(TenantAccountJoin).filter_by(account_id=service_account.id, current=True).first()
  255. )
  256. if not current_tenant:
  257. raise ValueError(f"Current tenant not found for account {service_account.id}")
  258. service_account.set_tenant_id(current_tenant.tenant_id)
  259. workflow_node_execution_repository = SQLAlchemyWorkflowNodeExecutionRepository(
  260. session_factory=session_factory,
  261. user=service_account,
  262. app_id=trace_info.metadata.get("app_id"),
  263. triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN,
  264. )
  265. # Get all executions for this workflow run
  266. workflow_node_executions = workflow_node_execution_repository.get_by_workflow_run(
  267. workflow_run_id=trace_info.workflow_run_id
  268. )
  269. return workflow_node_executions
  270. def build_workflow_node_span(
  271. self, node_execution: WorkflowNodeExecution, trace_id: int, trace_info: WorkflowTraceInfo, workflow_span_id: int
  272. ):
  273. try:
  274. if node_execution.node_type == NodeType.LLM:
  275. node_span = self.build_workflow_llm_span(trace_id, workflow_span_id, trace_info, node_execution)
  276. elif node_execution.node_type == NodeType.KNOWLEDGE_RETRIEVAL:
  277. node_span = self.build_workflow_retrieval_span(trace_id, workflow_span_id, trace_info, node_execution)
  278. elif node_execution.node_type == NodeType.TOOL:
  279. node_span = self.build_workflow_tool_span(trace_id, workflow_span_id, trace_info, node_execution)
  280. else:
  281. node_span = self.build_workflow_task_span(trace_id, workflow_span_id, trace_info, node_execution)
  282. return node_span
  283. except Exception as e:
  284. logger.debug("Error occurred in build_workflow_node_span: %s", e, exc_info=True)
  285. return None
  286. def get_workflow_node_status(self, node_execution: WorkflowNodeExecution) -> Status:
  287. span_status: Status = Status(StatusCode.UNSET)
  288. if node_execution.status == WorkflowNodeExecutionStatus.SUCCEEDED:
  289. span_status = Status(StatusCode.OK)
  290. elif node_execution.status in [WorkflowNodeExecutionStatus.FAILED, WorkflowNodeExecutionStatus.EXCEPTION]:
  291. span_status = Status(StatusCode.ERROR, str(node_execution.error))
  292. return span_status
  293. def build_workflow_task_span(
  294. self, trace_id: int, workflow_span_id: int, trace_info: WorkflowTraceInfo, node_execution: WorkflowNodeExecution
  295. ) -> SpanData:
  296. return SpanData(
  297. trace_id=trace_id,
  298. parent_span_id=workflow_span_id,
  299. span_id=convert_to_span_id(node_execution.id, "node"),
  300. name=node_execution.title,
  301. start_time=convert_datetime_to_nanoseconds(node_execution.created_at),
  302. end_time=convert_datetime_to_nanoseconds(node_execution.finished_at),
  303. attributes={
  304. GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id") or "",
  305. GEN_AI_SPAN_KIND: GenAISpanKind.TASK.value,
  306. GEN_AI_FRAMEWORK: "dify",
  307. INPUT_VALUE: json.dumps(node_execution.inputs, ensure_ascii=False),
  308. OUTPUT_VALUE: json.dumps(node_execution.outputs, ensure_ascii=False),
  309. },
  310. status=self.get_workflow_node_status(node_execution),
  311. )
  312. def build_workflow_tool_span(
  313. self, trace_id: int, workflow_span_id: int, trace_info: WorkflowTraceInfo, node_execution: WorkflowNodeExecution
  314. ) -> SpanData:
  315. tool_des = {}
  316. if node_execution.metadata:
  317. tool_des = node_execution.metadata.get(WorkflowNodeExecutionMetadataKey.TOOL_INFO, {})
  318. return SpanData(
  319. trace_id=trace_id,
  320. parent_span_id=workflow_span_id,
  321. span_id=convert_to_span_id(node_execution.id, "node"),
  322. name=node_execution.title,
  323. start_time=convert_datetime_to_nanoseconds(node_execution.created_at),
  324. end_time=convert_datetime_to_nanoseconds(node_execution.finished_at),
  325. attributes={
  326. GEN_AI_SPAN_KIND: GenAISpanKind.TOOL.value,
  327. GEN_AI_FRAMEWORK: "dify",
  328. TOOL_NAME: node_execution.title,
  329. TOOL_DESCRIPTION: json.dumps(tool_des, ensure_ascii=False),
  330. TOOL_PARAMETERS: json.dumps(node_execution.inputs if node_execution.inputs else {}, ensure_ascii=False),
  331. INPUT_VALUE: json.dumps(node_execution.inputs if node_execution.inputs else {}, ensure_ascii=False),
  332. OUTPUT_VALUE: json.dumps(node_execution.outputs, ensure_ascii=False),
  333. },
  334. status=self.get_workflow_node_status(node_execution),
  335. )
  336. def build_workflow_retrieval_span(
  337. self, trace_id: int, workflow_span_id: int, trace_info: WorkflowTraceInfo, node_execution: WorkflowNodeExecution
  338. ) -> SpanData:
  339. input_value = ""
  340. if node_execution.inputs:
  341. input_value = str(node_execution.inputs.get("query", ""))
  342. output_value = ""
  343. if node_execution.outputs:
  344. output_value = json.dumps(node_execution.outputs.get("result", []), ensure_ascii=False)
  345. return SpanData(
  346. trace_id=trace_id,
  347. parent_span_id=workflow_span_id,
  348. span_id=convert_to_span_id(node_execution.id, "node"),
  349. name=node_execution.title,
  350. start_time=convert_datetime_to_nanoseconds(node_execution.created_at),
  351. end_time=convert_datetime_to_nanoseconds(node_execution.finished_at),
  352. attributes={
  353. GEN_AI_SPAN_KIND: GenAISpanKind.RETRIEVER.value,
  354. GEN_AI_FRAMEWORK: "dify",
  355. RETRIEVAL_QUERY: input_value,
  356. RETRIEVAL_DOCUMENT: output_value,
  357. INPUT_VALUE: input_value,
  358. OUTPUT_VALUE: output_value,
  359. },
  360. status=self.get_workflow_node_status(node_execution),
  361. )
  362. def build_workflow_llm_span(
  363. self, trace_id: int, workflow_span_id: int, trace_info: WorkflowTraceInfo, node_execution: WorkflowNodeExecution
  364. ) -> SpanData:
  365. process_data = node_execution.process_data or {}
  366. outputs = node_execution.outputs or {}
  367. usage_data = process_data.get("usage", {}) if "usage" in process_data else outputs.get("usage", {})
  368. return SpanData(
  369. trace_id=trace_id,
  370. parent_span_id=workflow_span_id,
  371. span_id=convert_to_span_id(node_execution.id, "node"),
  372. name=node_execution.title,
  373. start_time=convert_datetime_to_nanoseconds(node_execution.created_at),
  374. end_time=convert_datetime_to_nanoseconds(node_execution.finished_at),
  375. attributes={
  376. GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id") or "",
  377. GEN_AI_SPAN_KIND: GenAISpanKind.LLM.value,
  378. GEN_AI_FRAMEWORK: "dify",
  379. GEN_AI_MODEL_NAME: process_data.get("model_name") or "",
  380. GEN_AI_SYSTEM: process_data.get("model_provider") or "",
  381. GEN_AI_USAGE_INPUT_TOKENS: str(usage_data.get("prompt_tokens", 0)),
  382. GEN_AI_USAGE_OUTPUT_TOKENS: str(usage_data.get("completion_tokens", 0)),
  383. GEN_AI_USAGE_TOTAL_TOKENS: str(usage_data.get("total_tokens", 0)),
  384. GEN_AI_PROMPT: json.dumps(process_data.get("prompts", []), ensure_ascii=False),
  385. GEN_AI_COMPLETION: str(outputs.get("text", "")),
  386. GEN_AI_RESPONSE_FINISH_REASON: outputs.get("finish_reason") or "",
  387. INPUT_VALUE: json.dumps(process_data.get("prompts", []), ensure_ascii=False),
  388. OUTPUT_VALUE: str(outputs.get("text", "")),
  389. },
  390. status=self.get_workflow_node_status(node_execution),
  391. )
  392. def add_workflow_span(
  393. self, trace_id: int, workflow_span_id: int, trace_info: WorkflowTraceInfo, links: Sequence[Link]
  394. ):
  395. message_span_id = None
  396. if trace_info.message_id:
  397. message_span_id = convert_to_span_id(trace_info.message_id, "message")
  398. user_id = trace_info.metadata.get("user_id")
  399. status: Status = Status(StatusCode.OK)
  400. if trace_info.error:
  401. status = Status(StatusCode.ERROR, trace_info.error)
  402. if message_span_id: # chatflow
  403. message_span = SpanData(
  404. trace_id=trace_id,
  405. parent_span_id=None,
  406. span_id=message_span_id,
  407. name="message",
  408. start_time=convert_datetime_to_nanoseconds(trace_info.start_time),
  409. end_time=convert_datetime_to_nanoseconds(trace_info.end_time),
  410. attributes={
  411. GEN_AI_SESSION_ID: trace_info.metadata.get("conversation_id") or "",
  412. GEN_AI_USER_ID: str(user_id),
  413. GEN_AI_SPAN_KIND: GenAISpanKind.CHAIN.value,
  414. GEN_AI_FRAMEWORK: "dify",
  415. INPUT_VALUE: trace_info.workflow_run_inputs.get("sys.query") or "",
  416. OUTPUT_VALUE: json.dumps(trace_info.workflow_run_outputs, ensure_ascii=False),
  417. },
  418. status=status,
  419. links=links,
  420. )
  421. self.trace_client.add_span(message_span)
  422. workflow_span = SpanData(
  423. trace_id=trace_id,
  424. parent_span_id=message_span_id,
  425. span_id=workflow_span_id,
  426. name="workflow",
  427. start_time=convert_datetime_to_nanoseconds(trace_info.start_time),
  428. end_time=convert_datetime_to_nanoseconds(trace_info.end_time),
  429. attributes={
  430. GEN_AI_USER_ID: str(user_id),
  431. GEN_AI_SPAN_KIND: GenAISpanKind.CHAIN.value,
  432. GEN_AI_FRAMEWORK: "dify",
  433. INPUT_VALUE: json.dumps(trace_info.workflow_run_inputs, ensure_ascii=False),
  434. OUTPUT_VALUE: json.dumps(trace_info.workflow_run_outputs, ensure_ascii=False),
  435. },
  436. status=status,
  437. links=links,
  438. )
  439. self.trace_client.add_span(workflow_span)
  440. def suggested_question_trace(self, trace_info: SuggestedQuestionTraceInfo):
  441. message_id = trace_info.message_id
  442. status: Status = Status(StatusCode.OK)
  443. if trace_info.error:
  444. status = Status(StatusCode.ERROR, trace_info.error)
  445. trace_id = convert_to_trace_id(message_id)
  446. links = []
  447. if trace_info.trace_id:
  448. links.append(create_link(trace_id_str=trace_info.trace_id))
  449. suggested_question_span = SpanData(
  450. trace_id=trace_id,
  451. parent_span_id=convert_to_span_id(message_id, "message"),
  452. span_id=convert_to_span_id(message_id, "suggested_question"),
  453. name="suggested_question",
  454. start_time=convert_datetime_to_nanoseconds(trace_info.start_time),
  455. end_time=convert_datetime_to_nanoseconds(trace_info.end_time),
  456. attributes={
  457. GEN_AI_SPAN_KIND: GenAISpanKind.LLM.value,
  458. GEN_AI_FRAMEWORK: "dify",
  459. GEN_AI_MODEL_NAME: trace_info.metadata.get("ls_model_name") or "",
  460. GEN_AI_SYSTEM: trace_info.metadata.get("ls_provider") or "",
  461. GEN_AI_PROMPT: json.dumps(trace_info.inputs, ensure_ascii=False),
  462. GEN_AI_COMPLETION: json.dumps(trace_info.suggested_question, ensure_ascii=False),
  463. INPUT_VALUE: json.dumps(trace_info.inputs, ensure_ascii=False),
  464. OUTPUT_VALUE: json.dumps(trace_info.suggested_question, ensure_ascii=False),
  465. },
  466. status=status,
  467. links=links,
  468. )
  469. self.trace_client.add_span(suggested_question_span)
  470. def extract_retrieval_documents(documents: list[Document]):
  471. documents_data = []
  472. for document in documents:
  473. document_data = {
  474. "content": document.page_content,
  475. "metadata": {
  476. "dataset_id": document.metadata.get("dataset_id"),
  477. "doc_id": document.metadata.get("doc_id"),
  478. "document_id": document.metadata.get("document_id"),
  479. },
  480. "score": document.metadata.get("score"),
  481. }
  482. documents_data.append(document_data)
  483. return documents_data