weave_trace.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  1. import logging
  2. import os
  3. import uuid
  4. from datetime import datetime, timedelta
  5. from typing import Any, Optional, cast
  6. import wandb
  7. import weave
  8. from sqlalchemy.orm import sessionmaker
  9. from core.ops.base_trace_instance import BaseTraceInstance
  10. from core.ops.entities.config_entity import WeaveConfig
  11. from core.ops.entities.trace_entity import (
  12. BaseTraceInfo,
  13. DatasetRetrievalTraceInfo,
  14. GenerateNameTraceInfo,
  15. MessageTraceInfo,
  16. ModerationTraceInfo,
  17. SuggestedQuestionTraceInfo,
  18. ToolTraceInfo,
  19. TraceTaskName,
  20. WorkflowTraceInfo,
  21. )
  22. from core.ops.weave_trace.entities.weave_trace_entity import WeaveTraceModel
  23. from core.repositories import DifyCoreRepositoryFactory
  24. from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey
  25. from core.workflow.nodes.enums import NodeType
  26. from extensions.ext_database import db
  27. from models import EndUser, MessageFile, WorkflowNodeExecutionTriggeredFrom
  28. logger = logging.getLogger(__name__)
  29. class WeaveDataTrace(BaseTraceInstance):
  30. def __init__(
  31. self,
  32. weave_config: WeaveConfig,
  33. ):
  34. super().__init__(weave_config)
  35. self.weave_api_key = weave_config.api_key
  36. self.project_name = weave_config.project
  37. self.entity = weave_config.entity
  38. self.host = weave_config.host
  39. # Login with API key first, including host if provided
  40. if self.host:
  41. login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True, host=self.host)
  42. else:
  43. login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True)
  44. if not login_status:
  45. logger.error("Failed to login to Weights & Biases with the provided API key")
  46. raise ValueError("Weave login failed")
  47. # Then initialize weave client
  48. self.weave_client = weave.init(
  49. project_name=(f"{self.entity}/{self.project_name}" if self.entity else self.project_name)
  50. )
  51. self.file_base_url = os.getenv("FILES_URL", "http://127.0.0.1:5001")
  52. self.calls: dict[str, Any] = {}
  53. def get_project_url(
  54. self,
  55. ):
  56. try:
  57. project_url = f"https://wandb.ai/{self.weave_client._project_id()}"
  58. return project_url
  59. except Exception as e:
  60. logger.debug("Weave get run url failed: %s", str(e))
  61. raise ValueError(f"Weave get run url failed: {str(e)}")
  62. def trace(self, trace_info: BaseTraceInfo):
  63. logger.debug("Trace info: %s", trace_info)
  64. if isinstance(trace_info, WorkflowTraceInfo):
  65. self.workflow_trace(trace_info)
  66. if isinstance(trace_info, MessageTraceInfo):
  67. self.message_trace(trace_info)
  68. if isinstance(trace_info, ModerationTraceInfo):
  69. self.moderation_trace(trace_info)
  70. if isinstance(trace_info, SuggestedQuestionTraceInfo):
  71. self.suggested_question_trace(trace_info)
  72. if isinstance(trace_info, DatasetRetrievalTraceInfo):
  73. self.dataset_retrieval_trace(trace_info)
  74. if isinstance(trace_info, ToolTraceInfo):
  75. self.tool_trace(trace_info)
  76. if isinstance(trace_info, GenerateNameTraceInfo):
  77. self.generate_name_trace(trace_info)
  78. def workflow_trace(self, trace_info: WorkflowTraceInfo):
  79. trace_id = trace_info.trace_id or trace_info.message_id or trace_info.workflow_run_id
  80. if trace_info.start_time is None:
  81. trace_info.start_time = datetime.now()
  82. if trace_info.message_id:
  83. message_attributes = trace_info.metadata
  84. message_attributes["workflow_app_log_id"] = trace_info.workflow_app_log_id
  85. message_attributes["message_id"] = trace_info.message_id
  86. message_attributes["workflow_run_id"] = trace_info.workflow_run_id
  87. message_attributes["trace_id"] = trace_id
  88. message_attributes["start_time"] = trace_info.start_time
  89. message_attributes["end_time"] = trace_info.end_time
  90. message_attributes["tags"] = ["message", "workflow"]
  91. message_run = WeaveTraceModel(
  92. id=trace_info.message_id,
  93. op=str(TraceTaskName.MESSAGE_TRACE.value),
  94. inputs=dict(trace_info.workflow_run_inputs),
  95. outputs=dict(trace_info.workflow_run_outputs),
  96. total_tokens=trace_info.total_tokens,
  97. attributes=message_attributes,
  98. exception=trace_info.error,
  99. file_list=[],
  100. )
  101. self.start_call(message_run, parent_run_id=trace_info.workflow_run_id)
  102. self.finish_call(message_run)
  103. workflow_attributes = trace_info.metadata
  104. workflow_attributes["workflow_run_id"] = trace_info.workflow_run_id
  105. workflow_attributes["trace_id"] = trace_id
  106. workflow_attributes["start_time"] = trace_info.start_time
  107. workflow_attributes["end_time"] = trace_info.end_time
  108. workflow_attributes["tags"] = ["workflow"]
  109. workflow_run = WeaveTraceModel(
  110. file_list=trace_info.file_list,
  111. total_tokens=trace_info.total_tokens,
  112. id=trace_info.workflow_run_id,
  113. op=str(TraceTaskName.WORKFLOW_TRACE.value),
  114. inputs=dict(trace_info.workflow_run_inputs),
  115. outputs=dict(trace_info.workflow_run_outputs),
  116. attributes=workflow_attributes,
  117. exception=trace_info.error,
  118. )
  119. self.start_call(workflow_run, parent_run_id=trace_info.message_id)
  120. # through workflow_run_id get all_nodes_execution using repository
  121. session_factory = sessionmaker(bind=db.engine)
  122. # Find the app's creator account
  123. app_id = trace_info.metadata.get("app_id")
  124. if not app_id:
  125. raise ValueError("No app_id found in trace_info metadata")
  126. service_account = self.get_service_account_with_tenant(app_id)
  127. workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository(
  128. session_factory=session_factory,
  129. user=service_account,
  130. app_id=app_id,
  131. triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN,
  132. )
  133. # Get all executions for this workflow run
  134. workflow_node_executions = workflow_node_execution_repository.get_by_workflow_run(
  135. workflow_run_id=trace_info.workflow_run_id
  136. )
  137. for node_execution in workflow_node_executions:
  138. node_execution_id = node_execution.id
  139. tenant_id = trace_info.tenant_id # Use from trace_info instead
  140. app_id = trace_info.metadata.get("app_id") # Use from trace_info instead
  141. node_name = node_execution.title
  142. node_type = node_execution.node_type
  143. status = node_execution.status
  144. if node_type == NodeType.LLM:
  145. inputs = node_execution.process_data.get("prompts", {}) if node_execution.process_data else {}
  146. else:
  147. inputs = node_execution.inputs if node_execution.inputs else {}
  148. outputs = node_execution.outputs if node_execution.outputs else {}
  149. created_at = node_execution.created_at or datetime.now()
  150. elapsed_time = node_execution.elapsed_time
  151. finished_at = created_at + timedelta(seconds=elapsed_time)
  152. execution_metadata = node_execution.metadata if node_execution.metadata else {}
  153. node_total_tokens = execution_metadata.get(WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS) or 0
  154. attributes = {str(k): v for k, v in execution_metadata.items()}
  155. attributes.update(
  156. {
  157. "workflow_run_id": trace_info.workflow_run_id,
  158. "node_execution_id": node_execution_id,
  159. "tenant_id": tenant_id,
  160. "app_id": app_id,
  161. "app_name": node_name,
  162. "node_type": node_type,
  163. "status": status,
  164. }
  165. )
  166. process_data = node_execution.process_data if node_execution.process_data else {}
  167. if process_data and process_data.get("model_mode") == "chat":
  168. attributes.update(
  169. {
  170. "ls_provider": process_data.get("model_provider", ""),
  171. "ls_model_name": process_data.get("model_name", ""),
  172. }
  173. )
  174. attributes["tags"] = ["node_execution"]
  175. attributes["start_time"] = created_at
  176. attributes["end_time"] = finished_at
  177. attributes["elapsed_time"] = elapsed_time
  178. attributes["workflow_run_id"] = trace_info.workflow_run_id
  179. attributes["trace_id"] = trace_id
  180. node_run = WeaveTraceModel(
  181. total_tokens=node_total_tokens,
  182. op=node_type,
  183. inputs=inputs,
  184. outputs=outputs,
  185. file_list=trace_info.file_list,
  186. attributes=attributes,
  187. id=node_execution_id,
  188. exception=None,
  189. )
  190. self.start_call(node_run, parent_run_id=trace_info.workflow_run_id)
  191. self.finish_call(node_run)
  192. self.finish_call(workflow_run)
  193. def message_trace(self, trace_info: MessageTraceInfo):
  194. # get message file data
  195. file_list = cast(list[str], trace_info.file_list) or []
  196. message_file_data: Optional[MessageFile] = trace_info.message_file_data
  197. file_url = f"{self.file_base_url}/{message_file_data.url}" if message_file_data else ""
  198. file_list.append(file_url)
  199. attributes = trace_info.metadata
  200. message_data = trace_info.message_data
  201. if message_data is None:
  202. return
  203. message_id = message_data.id
  204. user_id = message_data.from_account_id
  205. attributes["user_id"] = user_id
  206. if message_data.from_end_user_id:
  207. end_user_data: Optional[EndUser] = (
  208. db.session.query(EndUser).where(EndUser.id == message_data.from_end_user_id).first()
  209. )
  210. if end_user_data is not None:
  211. end_user_id = end_user_data.session_id
  212. attributes["end_user_id"] = end_user_id
  213. attributes["message_id"] = message_id
  214. attributes["start_time"] = trace_info.start_time
  215. attributes["end_time"] = trace_info.end_time
  216. attributes["tags"] = ["message", str(trace_info.conversation_mode)]
  217. trace_id = trace_info.trace_id or message_id
  218. attributes["trace_id"] = trace_id
  219. message_run = WeaveTraceModel(
  220. id=trace_id,
  221. op=str(TraceTaskName.MESSAGE_TRACE.value),
  222. input_tokens=trace_info.message_tokens,
  223. output_tokens=trace_info.answer_tokens,
  224. total_tokens=trace_info.total_tokens,
  225. inputs=trace_info.inputs,
  226. outputs=trace_info.outputs,
  227. exception=trace_info.error,
  228. file_list=file_list,
  229. attributes=attributes,
  230. )
  231. self.start_call(message_run)
  232. # create llm run parented to message run
  233. llm_run = WeaveTraceModel(
  234. id=str(uuid.uuid4()),
  235. input_tokens=trace_info.message_tokens,
  236. output_tokens=trace_info.answer_tokens,
  237. total_tokens=trace_info.total_tokens,
  238. op="llm",
  239. inputs=trace_info.inputs,
  240. outputs=trace_info.outputs,
  241. attributes=attributes,
  242. file_list=[],
  243. exception=None,
  244. )
  245. self.start_call(
  246. llm_run,
  247. parent_run_id=trace_id,
  248. )
  249. self.finish_call(llm_run)
  250. self.finish_call(message_run)
  251. def moderation_trace(self, trace_info: ModerationTraceInfo):
  252. if trace_info.message_data is None:
  253. return
  254. attributes = trace_info.metadata
  255. attributes["tags"] = ["moderation"]
  256. attributes["message_id"] = trace_info.message_id
  257. attributes["start_time"] = trace_info.start_time or trace_info.message_data.created_at
  258. attributes["end_time"] = trace_info.end_time or trace_info.message_data.updated_at
  259. trace_id = trace_info.trace_id or trace_info.message_id
  260. attributes["trace_id"] = trace_id
  261. moderation_run = WeaveTraceModel(
  262. id=str(uuid.uuid4()),
  263. op=str(TraceTaskName.MODERATION_TRACE.value),
  264. inputs=trace_info.inputs,
  265. outputs={
  266. "action": trace_info.action,
  267. "flagged": trace_info.flagged,
  268. "preset_response": trace_info.preset_response,
  269. "inputs": trace_info.inputs,
  270. },
  271. attributes=attributes,
  272. exception=getattr(trace_info, "error", None),
  273. file_list=[],
  274. )
  275. self.start_call(moderation_run, parent_run_id=trace_id)
  276. self.finish_call(moderation_run)
  277. def suggested_question_trace(self, trace_info: SuggestedQuestionTraceInfo):
  278. message_data = trace_info.message_data
  279. if message_data is None:
  280. return
  281. attributes = trace_info.metadata
  282. attributes["message_id"] = trace_info.message_id
  283. attributes["tags"] = ["suggested_question"]
  284. attributes["start_time"] = (trace_info.start_time or message_data.created_at,)
  285. attributes["end_time"] = (trace_info.end_time or message_data.updated_at,)
  286. trace_id = trace_info.trace_id or trace_info.message_id
  287. attributes["trace_id"] = trace_id
  288. suggested_question_run = WeaveTraceModel(
  289. id=str(uuid.uuid4()),
  290. op=str(TraceTaskName.SUGGESTED_QUESTION_TRACE.value),
  291. inputs=trace_info.inputs,
  292. outputs=trace_info.suggested_question,
  293. attributes=attributes,
  294. exception=trace_info.error,
  295. file_list=[],
  296. )
  297. self.start_call(suggested_question_run, parent_run_id=trace_id)
  298. self.finish_call(suggested_question_run)
  299. def dataset_retrieval_trace(self, trace_info: DatasetRetrievalTraceInfo):
  300. if trace_info.message_data is None:
  301. return
  302. attributes = trace_info.metadata
  303. attributes["message_id"] = trace_info.message_id
  304. attributes["tags"] = ["dataset_retrieval"]
  305. attributes["start_time"] = (trace_info.start_time or trace_info.message_data.created_at,)
  306. attributes["end_time"] = (trace_info.end_time or trace_info.message_data.updated_at,)
  307. trace_id = trace_info.trace_id or trace_info.message_id
  308. attributes["trace_id"] = trace_id
  309. dataset_retrieval_run = WeaveTraceModel(
  310. id=str(uuid.uuid4()),
  311. op=str(TraceTaskName.DATASET_RETRIEVAL_TRACE.value),
  312. inputs=trace_info.inputs,
  313. outputs={"documents": trace_info.documents},
  314. attributes=attributes,
  315. exception=getattr(trace_info, "error", None),
  316. file_list=[],
  317. )
  318. self.start_call(dataset_retrieval_run, parent_run_id=trace_id)
  319. self.finish_call(dataset_retrieval_run)
  320. def tool_trace(self, trace_info: ToolTraceInfo):
  321. attributes = trace_info.metadata
  322. attributes["tags"] = ["tool", trace_info.tool_name]
  323. attributes["start_time"] = trace_info.start_time
  324. attributes["end_time"] = trace_info.end_time
  325. message_id = trace_info.message_id or getattr(trace_info, "conversation_id", None)
  326. message_id = message_id or None
  327. trace_id = trace_info.trace_id or message_id
  328. attributes["trace_id"] = trace_id
  329. tool_run = WeaveTraceModel(
  330. id=str(uuid.uuid4()),
  331. op=trace_info.tool_name,
  332. inputs=trace_info.tool_inputs,
  333. outputs=trace_info.tool_outputs,
  334. file_list=[cast(str, trace_info.file_url)] if trace_info.file_url else [],
  335. attributes=attributes,
  336. exception=trace_info.error,
  337. )
  338. self.start_call(tool_run, parent_run_id=trace_id)
  339. self.finish_call(tool_run)
  340. def generate_name_trace(self, trace_info: GenerateNameTraceInfo):
  341. attributes = trace_info.metadata
  342. attributes["tags"] = ["generate_name"]
  343. attributes["start_time"] = trace_info.start_time
  344. attributes["end_time"] = trace_info.end_time
  345. name_run = WeaveTraceModel(
  346. id=str(uuid.uuid4()),
  347. op=str(TraceTaskName.GENERATE_NAME_TRACE.value),
  348. inputs=trace_info.inputs,
  349. outputs=trace_info.outputs,
  350. attributes=attributes,
  351. exception=getattr(trace_info, "error", None),
  352. file_list=[],
  353. )
  354. self.start_call(name_run)
  355. self.finish_call(name_run)
  356. def api_check(self):
  357. try:
  358. if self.host:
  359. login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True, host=self.host)
  360. else:
  361. login_status = wandb.login(key=self.weave_api_key, verify=True, relogin=True)
  362. if not login_status:
  363. raise ValueError("Weave login failed")
  364. else:
  365. print("Weave login successful")
  366. return True
  367. except Exception as e:
  368. logger.debug("Weave API check failed: %s", str(e))
  369. raise ValueError(f"Weave API check failed: {str(e)}")
  370. def start_call(self, run_data: WeaveTraceModel, parent_run_id: Optional[str] = None):
  371. call = self.weave_client.create_call(op=run_data.op, inputs=run_data.inputs, attributes=run_data.attributes)
  372. self.calls[run_data.id] = call
  373. if parent_run_id:
  374. self.calls[run_data.id].parent_id = parent_run_id
  375. def finish_call(self, run_data: WeaveTraceModel):
  376. call = self.calls.get(run_data.id)
  377. if call:
  378. self.weave_client.finish_call(call=call, output=run_data.outputs, exception=run_data.exception)
  379. else:
  380. raise ValueError(f"Call with id {run_data.id} not found")