Skip to content

Commit df08768

Browse files
committed
feat: add video_understand_node.py
1 parent d9cb533 commit df08768

2 files changed

Lines changed: 372 additions & 0 deletions

File tree

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# coding=utf-8
2+
from .video_understand_node import VideoUnderstandNode
Lines changed: 370 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,370 @@
1+
# coding=utf-8
2+
import uuid_utils.compat as uuid
3+
from functools import reduce
4+
5+
from django.db.models import QuerySet
6+
from django.utils.translation import gettext_lazy as _
7+
from langchain_core.messages import HumanMessage, SystemMessage, AIMessage
8+
from rest_framework import serializers
9+
10+
from application.workflow.common import WorkflowType
11+
from application.workflow.i_node import INode
12+
from application.workflow.message.struct.content import NodeInfo, Position
13+
from application.workflow.message.struct.reasoning_content import ReasoningContent
14+
from application.workflow.message.struct.text_content import TextContent
15+
from application.workflow.status import Status
16+
from application.workflow.tools import Reasoning
17+
from knowledge.models import File
18+
from models_provider.tools import get_model_instance_by_model_workspace_id
19+
20+
21+
class VideoUnderstandNodeSerializer(serializers.Serializer):
22+
model_id = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Model id"))
23+
model_id_type = serializers.CharField(required=False, default="custom", label=_("Model id type"))
24+
model_id_reference = serializers.ListField(
25+
required=False, child=serializers.CharField(), allow_empty=True, label=_("Reference Field")
26+
)
27+
system = serializers.CharField(required=False, allow_blank=True, allow_null=True, label=_("Role Setting"))
28+
prompt = serializers.CharField(required=True, label=_("Prompt word"))
29+
dialogue_number = serializers.IntegerField(required=True, label=_("Number of multi-round conversations"))
30+
dialogue_type = serializers.CharField(required=True, label=_("Conversation storage type"))
31+
is_result = serializers.BooleanField(required=False, label=_("Whether to return content"))
32+
video_list = serializers.ListField(required=False, label=_("video"))
33+
model_params_setting = serializers.JSONField(required=False, default=dict, label=_("Model parameter settings"))
34+
model_setting = serializers.DictField(required=False, label="Model settings")
35+
36+
37+
class VideoUnderstandNode(INode):
38+
serializer_class = VideoUnderstandNodeSerializer
39+
supported_workflow_type_list = [WorkflowType.APPLICATION, WorkflowType.KNOWLEDGE, WorkflowType.TOOL]
40+
type = "video-understand-node"
41+
42+
def execute(self):
43+
workflow_params = self.get_workflow_parameters()
44+
node_params = self.get_parameters()
45+
46+
model_id = node_params.get("model_id")
47+
model_id_type = node_params.get("model_id_type", "custom")
48+
model_id_reference = node_params.get("model_id_reference")
49+
system = node_params.get("system", "")
50+
prompt = node_params.get("prompt", "")
51+
dialogue_number = node_params.get("dialogue_number", 0)
52+
dialogue_type = node_params.get("dialogue_type", "WORKFLOW")
53+
is_result = node_params.get("is_result", False)
54+
video_list_ref = node_params.get("video_list")
55+
model_params_setting = node_params.get("model_params_setting")
56+
model_setting = node_params.get("model_setting")
57+
58+
workflow_type = self.get_workflow_type()
59+
if workflow_type in (WorkflowType.KNOWLEDGE, WorkflowType.TOOL):
60+
history_chat_record = []
61+
chat_id = None
62+
workspace_id = workflow_params.get("workspace_id")
63+
else:
64+
history_chat_record = workflow_params.get("history_chat_record", [])
65+
chat_id = workflow_params.get("chat_id")
66+
workspace_id = workflow_params.get("workspace_id")
67+
68+
if model_setting is None:
69+
model_setting = {
70+
"reasoning_content_enable": False,
71+
"reasoning_content_end": "</think>",
72+
"reasoning_content_start": "<think>",
73+
}
74+
self.write_context("model_setting", model_setting)
75+
76+
if model_id_type == "reference" and model_id_reference:
77+
reference_data = self.workflow_manage.get_reference_field(
78+
model_id_reference[0],
79+
model_id_reference[1:],
80+
)
81+
if reference_data and isinstance(reference_data, dict):
82+
model_id = reference_data.get("model_id", model_id)
83+
model_params_setting = reference_data.get("model_params_setting")
84+
85+
if not model_id:
86+
raise Exception(_("Model is not allowed to be empty"))
87+
88+
video = None
89+
if video_list_ref:
90+
video = self.workflow_manage.get_reference_field(video_list_ref[0], video_list_ref[1:])
91+
92+
chat_model = get_model_instance_by_model_workspace_id(model_id, workspace_id, **(model_params_setting or {}))
93+
94+
history_message_for_details = self._get_history_message_for_details(history_chat_record, dialogue_number)
95+
self.write_context(
96+
"history_message",
97+
[{"content": message.content, "role": message.type} for message in (history_message_for_details or [])],
98+
)
99+
100+
question = self.workflow_manage.generate_prompt(prompt)
101+
self.write_context("question", question)
102+
103+
system = self.workflow_manage.generate_prompt(system)
104+
self.write_context("system", system)
105+
106+
history_message = self._get_history_message(history_chat_record, dialogue_number, chat_model)
107+
message_list = self._generate_message_list(chat_model, system, prompt, history_message, video)
108+
self.write_context(
109+
"message_list",
110+
[{"content": m.content, "role": m.type} for m in message_list],
111+
)
112+
113+
self._generate_context_video(video)
114+
self.write_context("dialogue_type", dialogue_type)
115+
116+
reasoning_content_id = str(uuid.uuid7())
117+
text_content_id = str(uuid.uuid7())
118+
119+
node_info = NodeInfo(self.get_node_id(), self.get_node_name(), Status.RUNNING)
120+
121+
r = chat_model.stream(message_list)
122+
self._stream_response(
123+
r, chat_model, message_list, question, reasoning_content_id, text_content_id, node_info, is_result
124+
)
125+
126+
def _stream_response(
127+
self, response, chat_model, message_list, question, reasoning_content_id, text_content_id, node_info, is_result
128+
):
129+
model_setting = self.get_context("model_setting") or {}
130+
reasoning = Reasoning(
131+
model_setting.get("reasoning_content_start", "<think>"),
132+
model_setting.get("reasoning_content_end", "</think>"),
133+
)
134+
answer = ""
135+
reasoning_content = ""
136+
response_reasoning_content = False
137+
138+
for chunk in response:
139+
self._check_cancelled()
140+
reasoning_chunk = reasoning.get_reasoning_content(chunk)
141+
content_chunk = reasoning_chunk.get("content")
142+
if "reasoning_content" in chunk.additional_kwargs:
143+
response_reasoning_content = True
144+
reasoning_content_chunk = chunk.additional_kwargs.get("reasoning_content", "")
145+
else:
146+
reasoning_content_chunk = reasoning_chunk.get("reasoning_content")
147+
answer += content_chunk
148+
if reasoning_content_chunk is None:
149+
reasoning_content_chunk = ""
150+
reasoning_content += reasoning_content_chunk
151+
152+
if is_result:
153+
if isinstance(chunk.content, list):
154+
for chunk_item in chunk.content:
155+
text = chunk_item.get("text", "")
156+
if text:
157+
self.write(
158+
TextContent(
159+
text_content_id, text, Status.RUNNING, node_info, Position(self.get_node_id())
160+
)
161+
)
162+
if reasoning_content_chunk and model_setting.get("reasoning_content_enable", False):
163+
self.write(
164+
ReasoningContent(
165+
reasoning_content_id,
166+
reasoning_content_chunk,
167+
Status.RUNNING,
168+
node_info,
169+
Position(self.get_node_id()),
170+
)
171+
)
172+
else:
173+
if content_chunk:
174+
self.write(
175+
TextContent(
176+
text_content_id, content_chunk, Status.RUNNING, node_info, Position(self.get_node_id())
177+
)
178+
)
179+
if reasoning_content_chunk and model_setting.get("reasoning_content_enable", False):
180+
self.write(
181+
ReasoningContent(
182+
reasoning_content_id,
183+
reasoning_content_chunk,
184+
Status.RUNNING,
185+
node_info,
186+
Position(self.get_node_id()),
187+
)
188+
)
189+
190+
reasoning_end = reasoning.get_end_reasoning_content()
191+
answer += reasoning_end.get("content")
192+
reasoning_content_chunk = ""
193+
if not response_reasoning_content:
194+
reasoning_content_chunk = reasoning_end.get("reasoning_content")
195+
if is_result:
196+
if reasoning_end.get("content"):
197+
self.write(
198+
TextContent(
199+
text_content_id,
200+
reasoning_end.get("content"),
201+
Status.RUNNING,
202+
node_info,
203+
Position(self.get_node_id()),
204+
)
205+
)
206+
if reasoning_content_chunk and model_setting.get("reasoning_content_enable", False):
207+
self.write(
208+
ReasoningContent(
209+
reasoning_content_id,
210+
reasoning_content_chunk,
211+
Status.RUNNING,
212+
node_info,
213+
Position(self.get_node_id()),
214+
)
215+
)
216+
217+
self._write_final_context(chat_model, message_list, question, answer, reasoning_content)
218+
219+
def _write_final_context(self, chat_model, message_list, question, answer, reasoning_content):
220+
message_tokens = chat_model.get_num_tokens_from_messages(message_list)
221+
answer_tokens = chat_model.get_num_tokens(answer)
222+
self.write_context("message_tokens", message_tokens)
223+
self.write_context("answer_tokens", answer_tokens)
224+
self.write_context("answer", answer)
225+
self.write_context("question", question)
226+
self.write_context("reasoning_content", reasoning_content)
227+
228+
def _generate_context_video(self, video):
229+
if isinstance(video, str) and video.startswith("http"):
230+
self.write_context("video_list", [{"url": video}])
231+
elif video is not None and len(video) > 0:
232+
self.write_context("video_list", video)
233+
234+
def _get_history_message_for_details(self, history_chat_record, dialogue_number):
235+
start_index = len(history_chat_record) - dialogue_number
236+
history_message = reduce(
237+
lambda x, y: [*x, *y],
238+
[
239+
[
240+
self._generate_history_human_message_for_details(history_chat_record[index]),
241+
*self._generate_history_ai_message(history_chat_record[index]),
242+
]
243+
for index in range(start_index if start_index > 0 else 0, len(history_chat_record))
244+
],
245+
[],
246+
)
247+
return history_message
248+
249+
def _generate_history_ai_message(self, chat_record):
250+
for val in chat_record.details.values():
251+
if self.node.id == val["node_id"] and "video_list" in val:
252+
if val["dialogue_type"] == "WORKFLOW":
253+
return chat_record.get_ai_message()
254+
return [AIMessage(content=val["answer"])]
255+
return chat_record.get_ai_message()
256+
257+
def _generate_history_human_message_for_details(self, chat_record):
258+
for data in chat_record.details.values():
259+
if self.node.id == data["node_id"] and "video_list" in data:
260+
video_list = data["video_list"] or []
261+
if len(video_list) == 0 or data["dialogue_type"] == "WORKFLOW":
262+
return HumanMessage(content=chat_record.problem_text)
263+
file_id_list = []
264+
url_list = []
265+
for video in video_list:
266+
if "file_id" in video:
267+
file_id_list.append(video.get("file_id"))
268+
elif "url" in video:
269+
url_list.append(video.get("url"))
270+
return HumanMessage(
271+
content=[
272+
{"type": "text", "text": data["question"]},
273+
*[
274+
{"type": "video_url", "video_url": {"url": f"./oss/file/{file_id}"}}
275+
for file_id in file_id_list
276+
],
277+
*[{"type": "video_url", "video_url": {"url": url}} for url in url_list],
278+
]
279+
)
280+
return HumanMessage(content=chat_record.problem_text)
281+
282+
def _get_history_message(self, history_chat_record, dialogue_number, video_model):
283+
start_index = len(history_chat_record) - dialogue_number
284+
history_message = reduce(
285+
lambda x, y: [*x, *y],
286+
[
287+
[
288+
self._generate_history_human_message(history_chat_record[index], video_model),
289+
*self._generate_history_ai_message(history_chat_record[index]),
290+
]
291+
for index in range(start_index if start_index > 0 else 0, len(history_chat_record))
292+
],
293+
[],
294+
)
295+
return history_message
296+
297+
def _generate_history_human_message(self, chat_record, video_model):
298+
for data in chat_record.details.values():
299+
if self.node.id == data["node_id"] and "video_list" in data:
300+
video_list = data["video_list"] or []
301+
if len(video_list) == 0 or data["dialogue_type"] == "WORKFLOW":
302+
return HumanMessage(content=chat_record.problem_text)
303+
file_id_list = []
304+
url_list = []
305+
for video in video_list:
306+
if "file_id" in video:
307+
file_id_list.append(video.get("file_id"))
308+
elif "url" in video:
309+
url_list.append(video.get("url"))
310+
video_base64_list = [self._file_id_to_base64(file_id, video_model) for file_id in file_id_list]
311+
return HumanMessage(
312+
content=[
313+
{"type": "text", "text": data["question"]},
314+
*[
315+
{"type": "video_url", "video_url": {"url": base64_video}}
316+
for base64_video in video_base64_list
317+
],
318+
*[{"type": "video_url", "video_url": {"url": url}} for url in url_list],
319+
]
320+
)
321+
return HumanMessage(content=chat_record.problem_text)
322+
323+
@staticmethod
324+
def _file_id_to_base64(file_id: str, video_model):
325+
file = QuerySet(File).filter(id=file_id).first()
326+
file_bytes = file.get_bytes()
327+
return video_model.upload_file_and_get_url(file_bytes, file.file_name)
328+
329+
def _process_videos(self, video, video_model):
330+
videos = []
331+
if isinstance(video, str) and video.startswith("http"):
332+
videos.append({"type": "video_url", "video_url": {"url": video}})
333+
elif video is not None and len(video) > 0:
334+
for v in video:
335+
if "file_id" in v:
336+
file_id = v["file_id"]
337+
file = QuerySet(File).filter(id=file_id).first()
338+
url = video_model.upload_file_and_get_url(file.get_bytes(), file.file_name)
339+
videos.append({"type": "video_url", "video_url": {"url": url}})
340+
elif "url" in v and v["url"].startswith("http"):
341+
videos.append({"type": "video_url", "video_url": {"url": v["url"]}})
342+
return videos
343+
344+
def _generate_message_list(self, video_model, system: str, prompt: str, history_message, video):
345+
prompt_text = self.workflow_manage.generate_prompt(prompt)
346+
videos = self._process_videos(video, video_model)
347+
348+
if videos:
349+
messages = [HumanMessage(content=[{"type": "text", "text": prompt_text}, *videos])]
350+
else:
351+
messages = [HumanMessage(prompt_text)]
352+
353+
if system is not None and len(system) > 0:
354+
return [SystemMessage(system), *history_message, *messages]
355+
else:
356+
return [*history_message, *messages]
357+
358+
def get_details(self, index: int = 0, position: dict = None, old_details: dict = None, **kwargs):
359+
details = super().get_details(index, position, old_details, **kwargs)
360+
details.update(
361+
{
362+
"question": self.get_context("question"),
363+
"answer": self.get_context("answer"),
364+
"video_list": self.get_context("video_list"),
365+
"reasoning_content": self.get_context("reasoning_content"),
366+
"message_tokens": self.get_context("message_tokens"),
367+
"answer_tokens": self.get_context("answer_tokens"),
368+
}
369+
)
370+
return details

0 commit comments

Comments
 (0)