langsmith_trace.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526
  1. import logging
  2. import os
  3. import uuid
  4. from datetime import datetime, timedelta
  5. from typing import Optional, cast
  6. from langsmith import Client
  7. from langsmith.schemas import RunBase
  8. from sqlalchemy.orm import sessionmaker
  9. from core.ops.base_trace_instance import BaseTraceInstance
  10. from core.ops.entities.config_entity import LangSmithConfig
  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.langsmith_trace.entities.langsmith_trace_entity import (
  23. LangSmithRunModel,
  24. LangSmithRunType,
  25. LangSmithRunUpdateModel,
  26. )
  27. from core.ops.utils import filter_none_values, generate_dotted_order
  28. from core.repositories import DifyCoreRepositoryFactory
  29. from core.workflow.entities.workflow_node_execution import WorkflowNodeExecutionMetadataKey
  30. from core.workflow.nodes.enums import NodeType
  31. from extensions.ext_database import db
  32. from models import EndUser, MessageFile, WorkflowNodeExecutionTriggeredFrom
  33. logger = logging.getLogger(__name__)
  34. class LangSmithDataTrace(BaseTraceInstance):
  35. def __init__(
  36. self,
  37. langsmith_config: LangSmithConfig,
  38. ):
  39. super().__init__(langsmith_config)
  40. self.langsmith_key = langsmith_config.api_key
  41. self.project_name = langsmith_config.project
  42. self.project_id = None
  43. self.langsmith_client = Client(api_key=langsmith_config.api_key, api_url=langsmith_config.endpoint)
  44. self.file_base_url = os.getenv("FILES_URL", "http://127.0.0.1:5001")
  45. def trace(self, trace_info: BaseTraceInfo):
  46. if isinstance(trace_info, WorkflowTraceInfo):
  47. self.workflow_trace(trace_info)
  48. if isinstance(trace_info, MessageTraceInfo):
  49. self.message_trace(trace_info)
  50. if isinstance(trace_info, ModerationTraceInfo):
  51. self.moderation_trace(trace_info)
  52. if isinstance(trace_info, SuggestedQuestionTraceInfo):
  53. self.suggested_question_trace(trace_info)
  54. if isinstance(trace_info, DatasetRetrievalTraceInfo):
  55. self.dataset_retrieval_trace(trace_info)
  56. if isinstance(trace_info, ToolTraceInfo):
  57. self.tool_trace(trace_info)
  58. if isinstance(trace_info, GenerateNameTraceInfo):
  59. self.generate_name_trace(trace_info)
  60. def workflow_trace(self, trace_info: WorkflowTraceInfo):
  61. trace_id = trace_info.trace_id or trace_info.message_id or trace_info.workflow_run_id
  62. if trace_info.start_time is None:
  63. trace_info.start_time = datetime.now()
  64. message_dotted_order = (
  65. generate_dotted_order(trace_info.message_id, trace_info.start_time) if trace_info.message_id else None
  66. )
  67. workflow_dotted_order = generate_dotted_order(
  68. trace_info.workflow_run_id,
  69. trace_info.workflow_data.created_at,
  70. message_dotted_order,
  71. )
  72. metadata = trace_info.metadata
  73. metadata["workflow_app_log_id"] = trace_info.workflow_app_log_id
  74. if trace_info.message_id:
  75. message_run = LangSmithRunModel(
  76. id=trace_info.message_id,
  77. name=TraceTaskName.MESSAGE_TRACE.value,
  78. inputs=dict(trace_info.workflow_run_inputs),
  79. outputs=dict(trace_info.workflow_run_outputs),
  80. run_type=LangSmithRunType.chain,
  81. start_time=trace_info.start_time,
  82. end_time=trace_info.end_time,
  83. extra={
  84. "metadata": metadata,
  85. },
  86. tags=["message", "workflow"],
  87. error=trace_info.error,
  88. trace_id=trace_id,
  89. dotted_order=message_dotted_order,
  90. file_list=[],
  91. serialized=None,
  92. parent_run_id=None,
  93. events=[],
  94. session_id=None,
  95. session_name=None,
  96. reference_example_id=None,
  97. input_attachments={},
  98. output_attachments={},
  99. )
  100. self.add_run(message_run)
  101. langsmith_run = LangSmithRunModel(
  102. file_list=trace_info.file_list,
  103. total_tokens=trace_info.total_tokens,
  104. id=trace_info.workflow_run_id,
  105. name=TraceTaskName.WORKFLOW_TRACE.value,
  106. inputs=dict(trace_info.workflow_run_inputs),
  107. run_type=LangSmithRunType.tool,
  108. start_time=trace_info.workflow_data.created_at,
  109. end_time=trace_info.workflow_data.finished_at,
  110. outputs=dict(trace_info.workflow_run_outputs),
  111. extra={
  112. "metadata": metadata,
  113. },
  114. error=trace_info.error,
  115. tags=["workflow"],
  116. parent_run_id=trace_info.message_id or None,
  117. trace_id=trace_id,
  118. dotted_order=workflow_dotted_order,
  119. serialized=None,
  120. events=[],
  121. session_id=None,
  122. session_name=None,
  123. reference_example_id=None,
  124. input_attachments={},
  125. output_attachments={},
  126. )
  127. self.add_run(langsmith_run)
  128. # through workflow_run_id get all_nodes_execution using repository
  129. session_factory = sessionmaker(bind=db.engine)
  130. # Find the app's creator account
  131. app_id = trace_info.metadata.get("app_id")
  132. if not app_id:
  133. raise ValueError("No app_id found in trace_info metadata")
  134. service_account = self.get_service_account_with_tenant(app_id)
  135. workflow_node_execution_repository = DifyCoreRepositoryFactory.create_workflow_node_execution_repository(
  136. session_factory=session_factory,
  137. user=service_account,
  138. app_id=app_id,
  139. triggered_from=WorkflowNodeExecutionTriggeredFrom.WORKFLOW_RUN,
  140. )
  141. # Get all executions for this workflow run
  142. workflow_node_executions = workflow_node_execution_repository.get_by_workflow_run(
  143. workflow_run_id=trace_info.workflow_run_id
  144. )
  145. for node_execution in workflow_node_executions:
  146. node_execution_id = node_execution.id
  147. tenant_id = trace_info.tenant_id # Use from trace_info instead
  148. app_id = trace_info.metadata.get("app_id") # Use from trace_info instead
  149. node_name = node_execution.title
  150. node_type = node_execution.node_type
  151. status = node_execution.status
  152. if node_type == NodeType.LLM:
  153. inputs = node_execution.process_data.get("prompts", {}) if node_execution.process_data else {}
  154. else:
  155. inputs = node_execution.inputs if node_execution.inputs else {}
  156. outputs = node_execution.outputs if node_execution.outputs else {}
  157. created_at = node_execution.created_at or datetime.now()
  158. elapsed_time = node_execution.elapsed_time
  159. finished_at = created_at + timedelta(seconds=elapsed_time)
  160. execution_metadata = node_execution.metadata if node_execution.metadata else {}
  161. node_total_tokens = execution_metadata.get(WorkflowNodeExecutionMetadataKey.TOTAL_TOKENS) or 0
  162. metadata = {str(key): value for key, value in execution_metadata.items()}
  163. metadata.update(
  164. {
  165. "workflow_run_id": trace_info.workflow_run_id,
  166. "node_execution_id": node_execution_id,
  167. "tenant_id": tenant_id,
  168. "app_id": app_id,
  169. "app_name": node_name,
  170. "node_type": node_type,
  171. "status": status,
  172. }
  173. )
  174. process_data = node_execution.process_data if node_execution.process_data else {}
  175. if process_data and process_data.get("model_mode") == "chat":
  176. run_type = LangSmithRunType.llm
  177. metadata.update(
  178. {
  179. "ls_provider": process_data.get("model_provider", ""),
  180. "ls_model_name": process_data.get("model_name", ""),
  181. }
  182. )
  183. elif node_type == NodeType.KNOWLEDGE_RETRIEVAL:
  184. run_type = LangSmithRunType.retriever
  185. else:
  186. run_type = LangSmithRunType.tool
  187. prompt_tokens = 0
  188. completion_tokens = 0
  189. try:
  190. usage_data = process_data.get("usage", {}) if "usage" in process_data else outputs.get("usage", {})
  191. prompt_tokens = usage_data.get("prompt_tokens", 0)
  192. completion_tokens = usage_data.get("completion_tokens", 0)
  193. except Exception:
  194. logger.error("Failed to extract usage", exc_info=True)
  195. node_dotted_order = generate_dotted_order(node_execution_id, created_at, workflow_dotted_order)
  196. langsmith_run = LangSmithRunModel(
  197. total_tokens=node_total_tokens,
  198. input_tokens=prompt_tokens,
  199. output_tokens=completion_tokens,
  200. name=node_type,
  201. inputs=inputs,
  202. run_type=run_type,
  203. start_time=created_at,
  204. end_time=finished_at,
  205. outputs=outputs,
  206. file_list=trace_info.file_list,
  207. extra={
  208. "metadata": metadata,
  209. },
  210. parent_run_id=trace_info.workflow_run_id,
  211. tags=["node_execution"],
  212. id=node_execution_id,
  213. trace_id=trace_id,
  214. dotted_order=node_dotted_order,
  215. error="",
  216. serialized=None,
  217. events=[],
  218. session_id=None,
  219. session_name=None,
  220. reference_example_id=None,
  221. input_attachments={},
  222. output_attachments={},
  223. )
  224. self.add_run(langsmith_run)
  225. def message_trace(self, trace_info: MessageTraceInfo):
  226. # get message file data
  227. file_list = cast(list[str], trace_info.file_list) or []
  228. message_file_data: Optional[MessageFile] = trace_info.message_file_data
  229. file_url = f"{self.file_base_url}/{message_file_data.url}" if message_file_data else ""
  230. file_list.append(file_url)
  231. metadata = trace_info.metadata
  232. message_data = trace_info.message_data
  233. if message_data is None:
  234. return
  235. message_id = message_data.id
  236. user_id = message_data.from_account_id
  237. metadata["user_id"] = user_id
  238. if message_data.from_end_user_id:
  239. end_user_data: Optional[EndUser] = (
  240. db.session.query(EndUser).where(EndUser.id == message_data.from_end_user_id).first()
  241. )
  242. if end_user_data is not None:
  243. end_user_id = end_user_data.session_id
  244. metadata["end_user_id"] = end_user_id
  245. message_run = LangSmithRunModel(
  246. input_tokens=trace_info.message_tokens,
  247. output_tokens=trace_info.answer_tokens,
  248. total_tokens=trace_info.total_tokens,
  249. id=message_id,
  250. name=TraceTaskName.MESSAGE_TRACE.value,
  251. inputs=trace_info.inputs,
  252. run_type=LangSmithRunType.chain,
  253. start_time=trace_info.start_time,
  254. end_time=trace_info.end_time,
  255. outputs=message_data.answer,
  256. extra={"metadata": metadata},
  257. tags=["message", str(trace_info.conversation_mode)],
  258. error=trace_info.error,
  259. file_list=file_list,
  260. serialized=None,
  261. events=[],
  262. session_id=None,
  263. session_name=None,
  264. reference_example_id=None,
  265. input_attachments={},
  266. output_attachments={},
  267. trace_id=trace_info.trace_id,
  268. dotted_order=None,
  269. parent_run_id=None,
  270. )
  271. self.add_run(message_run)
  272. # create llm run parented to message run
  273. llm_run = LangSmithRunModel(
  274. input_tokens=trace_info.message_tokens,
  275. output_tokens=trace_info.answer_tokens,
  276. total_tokens=trace_info.total_tokens,
  277. name="llm",
  278. inputs=trace_info.inputs,
  279. run_type=LangSmithRunType.llm,
  280. start_time=trace_info.start_time,
  281. end_time=trace_info.end_time,
  282. outputs=message_data.answer,
  283. extra={"metadata": metadata},
  284. parent_run_id=message_id,
  285. tags=["llm", str(trace_info.conversation_mode)],
  286. error=trace_info.error,
  287. file_list=file_list,
  288. serialized=None,
  289. events=[],
  290. session_id=None,
  291. session_name=None,
  292. reference_example_id=None,
  293. input_attachments={},
  294. output_attachments={},
  295. trace_id=trace_info.trace_id,
  296. dotted_order=None,
  297. id=str(uuid.uuid4()),
  298. )
  299. self.add_run(llm_run)
  300. def moderation_trace(self, trace_info: ModerationTraceInfo):
  301. if trace_info.message_data is None:
  302. return
  303. langsmith_run = LangSmithRunModel(
  304. name=TraceTaskName.MODERATION_TRACE.value,
  305. inputs=trace_info.inputs,
  306. outputs={
  307. "action": trace_info.action,
  308. "flagged": trace_info.flagged,
  309. "preset_response": trace_info.preset_response,
  310. "inputs": trace_info.inputs,
  311. },
  312. run_type=LangSmithRunType.tool,
  313. extra={"metadata": trace_info.metadata},
  314. tags=["moderation"],
  315. parent_run_id=trace_info.message_id,
  316. start_time=trace_info.start_time or trace_info.message_data.created_at,
  317. end_time=trace_info.end_time or trace_info.message_data.updated_at,
  318. id=str(uuid.uuid4()),
  319. serialized=None,
  320. events=[],
  321. session_id=None,
  322. session_name=None,
  323. reference_example_id=None,
  324. input_attachments={},
  325. output_attachments={},
  326. trace_id=trace_info.trace_id,
  327. dotted_order=None,
  328. error="",
  329. file_list=[],
  330. )
  331. self.add_run(langsmith_run)
  332. def suggested_question_trace(self, trace_info: SuggestedQuestionTraceInfo):
  333. message_data = trace_info.message_data
  334. if message_data is None:
  335. return
  336. suggested_question_run = LangSmithRunModel(
  337. name=TraceTaskName.SUGGESTED_QUESTION_TRACE.value,
  338. inputs=trace_info.inputs,
  339. outputs=trace_info.suggested_question,
  340. run_type=LangSmithRunType.tool,
  341. extra={"metadata": trace_info.metadata},
  342. tags=["suggested_question"],
  343. parent_run_id=trace_info.message_id,
  344. start_time=trace_info.start_time or message_data.created_at,
  345. end_time=trace_info.end_time or message_data.updated_at,
  346. id=str(uuid.uuid4()),
  347. serialized=None,
  348. events=[],
  349. session_id=None,
  350. session_name=None,
  351. reference_example_id=None,
  352. input_attachments={},
  353. output_attachments={},
  354. trace_id=trace_info.trace_id,
  355. dotted_order=None,
  356. error="",
  357. file_list=[],
  358. )
  359. self.add_run(suggested_question_run)
  360. def dataset_retrieval_trace(self, trace_info: DatasetRetrievalTraceInfo):
  361. if trace_info.message_data is None:
  362. return
  363. dataset_retrieval_run = LangSmithRunModel(
  364. name=TraceTaskName.DATASET_RETRIEVAL_TRACE.value,
  365. inputs=trace_info.inputs,
  366. outputs={"documents": trace_info.documents},
  367. run_type=LangSmithRunType.retriever,
  368. extra={"metadata": trace_info.metadata},
  369. tags=["dataset_retrieval"],
  370. parent_run_id=trace_info.message_id,
  371. start_time=trace_info.start_time or trace_info.message_data.created_at,
  372. end_time=trace_info.end_time or trace_info.message_data.updated_at,
  373. id=str(uuid.uuid4()),
  374. serialized=None,
  375. events=[],
  376. session_id=None,
  377. session_name=None,
  378. reference_example_id=None,
  379. input_attachments={},
  380. output_attachments={},
  381. trace_id=trace_info.trace_id,
  382. dotted_order=None,
  383. error="",
  384. file_list=[],
  385. )
  386. self.add_run(dataset_retrieval_run)
  387. def tool_trace(self, trace_info: ToolTraceInfo):
  388. tool_run = LangSmithRunModel(
  389. name=trace_info.tool_name,
  390. inputs=trace_info.tool_inputs,
  391. outputs=trace_info.tool_outputs,
  392. run_type=LangSmithRunType.tool,
  393. extra={
  394. "metadata": trace_info.metadata,
  395. },
  396. tags=["tool", trace_info.tool_name],
  397. parent_run_id=trace_info.message_id,
  398. start_time=trace_info.start_time,
  399. end_time=trace_info.end_time,
  400. file_list=[cast(str, trace_info.file_url)],
  401. id=str(uuid.uuid4()),
  402. serialized=None,
  403. events=[],
  404. session_id=None,
  405. session_name=None,
  406. reference_example_id=None,
  407. input_attachments={},
  408. output_attachments={},
  409. trace_id=trace_info.trace_id,
  410. dotted_order=None,
  411. error=trace_info.error or "",
  412. )
  413. self.add_run(tool_run)
  414. def generate_name_trace(self, trace_info: GenerateNameTraceInfo):
  415. name_run = LangSmithRunModel(
  416. name=TraceTaskName.GENERATE_NAME_TRACE.value,
  417. inputs=trace_info.inputs,
  418. outputs=trace_info.outputs,
  419. run_type=LangSmithRunType.tool,
  420. extra={"metadata": trace_info.metadata},
  421. tags=["generate_name"],
  422. start_time=trace_info.start_time or datetime.now(),
  423. end_time=trace_info.end_time or datetime.now(),
  424. id=str(uuid.uuid4()),
  425. serialized=None,
  426. events=[],
  427. session_id=None,
  428. session_name=None,
  429. reference_example_id=None,
  430. input_attachments={},
  431. output_attachments={},
  432. trace_id=trace_info.trace_id,
  433. dotted_order=None,
  434. error="",
  435. file_list=[],
  436. parent_run_id=None,
  437. )
  438. self.add_run(name_run)
  439. def add_run(self, run_data: LangSmithRunModel):
  440. data = run_data.model_dump()
  441. if self.project_id:
  442. data["session_id"] = self.project_id
  443. elif self.project_name:
  444. data["session_name"] = self.project_name
  445. data = filter_none_values(data)
  446. try:
  447. self.langsmith_client.create_run(**data)
  448. logger.debug("LangSmith Run created successfully.")
  449. except Exception as e:
  450. raise ValueError(f"LangSmith Failed to create run: {str(e)}")
  451. def update_run(self, update_run_data: LangSmithRunUpdateModel):
  452. data = update_run_data.model_dump()
  453. data = filter_none_values(data)
  454. try:
  455. self.langsmith_client.update_run(**data)
  456. logger.debug("LangSmith Run updated successfully.")
  457. except Exception as e:
  458. raise ValueError(f"LangSmith Failed to update run: {str(e)}")
  459. def api_check(self):
  460. try:
  461. random_project_name = f"test_project_{datetime.now().strftime('%Y%m%d%H%M%S')}"
  462. self.langsmith_client.create_project(project_name=random_project_name)
  463. self.langsmith_client.delete_project(project_name=random_project_name)
  464. return True
  465. except Exception as e:
  466. logger.debug("LangSmith API check failed: %s", str(e))
  467. raise ValueError(f"LangSmith API check failed: {str(e)}")
  468. def get_project_url(self):
  469. try:
  470. run_data = RunBase(
  471. id=uuid.uuid4(),
  472. name="tool",
  473. inputs={"input": "test"},
  474. outputs={"output": "test"},
  475. run_type=LangSmithRunType.tool,
  476. start_time=datetime.now(),
  477. )
  478. project_url = self.langsmith_client.get_run_url(
  479. run=run_data, project_id=self.project_id, project_name=self.project_name
  480. )
  481. return project_url.split("/r/")[0]
  482. except Exception as e:
  483. logger.debug("LangSmith get run url failed: %s", str(e))
  484. raise ValueError(f"LangSmith get run url failed: {str(e)}")