aliyun_trace.py 23 KB

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