Files

73 lines
9.5 KiB
Python
Raw Permalink Normal View History

"""Create reproducible, importable Dify canvases from the application's prompts."""
import json
import argparse
import importlib.util
from pathlib import Path
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--provider-id', help='Native model tool provider ID created by dify-model-tools.py')
args = parser.parse_args()
ROOT = Path(__file__).resolve().parents[1]
DEST = ROOT / 'deploy/dify/workflows'
DEST.mkdir(parents=True, exist_ok=True)
events = (ROOT / 'internal/extraction/events.go').read_text(encoding='utf-8')
memoir = (ROOT / 'internal/memoir/generation.go').read_text(encoding='utf-8')
def constant(source, name): return source.split('const '+name+' = `', 1)[1].split('`', 1)[0]
extract_prompt = constant(events, 'eventExtractionPrompt')
daily_prompt = constant(events, 'dailyExtractionPrompt')
book_prompt = constant(memoir, 'MemoirPrompt')
def node(id, type, title, x, y, **data):
return dict(id=id, type='custom', position=dict(x=x,y=y), positionAbsolute=dict(x=x,y=y), width=244, height=110, sourcePosition='right', targetPosition='left', data=dict(type=type,title=title,desc='',selected=False,**data))
def edge(a,b,nodes,iteration=None):
types={n['id']:n['data']['type'] for n in nodes}
data=dict(sourceType=types[a],targetType=types[b],isInIteration=bool(iteration),isInLoop=False)
if iteration: data['iteration_id']=iteration
return dict(id=a+'-'+b,source=a,target=b,sourceHandle='source',targetHandle='target',type='custom',zIndex=1002 if iteration else 0,data=data)
def start():
return node('start','start','开始:接收后台任务',50,80,variables=[dict(variable=v,label=l,type='text-input',required=False,max_length=128,options=[],default='') for v,l in [('context_id','任务上下文(后台自动传入;留空可测试示例)'),('step_token','临时执行凭证(留空可测试示例)')]])
def http(id,title,step,x,y,kind=None,prompt=None,prepared=True,index=None):
selector='prepared' if prepared else 'start'
body={'context_id':'{{#'+selector+'.context_id#}}','step_token':'{{#'+selector+'.step_token#}}'}
if kind:body['kind']=kind
if prompt:body['prompt']=prompt
text=json.dumps(body,ensure_ascii=False)
if index is not None:text=text[:-1]+',"chapter_index":{{#pictures.item#}}}'
return node(id,'http-request',title,x,y,method='POST',url='http://model-bridge:8188/steps/'+step,authorization={'type':'no-auth'},headers='Content-Type:application/json',params='',body={'type':'json','data':[{'id':'body','key':'','type':'text','value':text}]},timeout={'connect':15,'read':240,'write':30},retry_config={'enabled':False,'max_retries':1,'retry_interval':1000})
def parse(id,title,source,fields,x,y,allow_errors=False):
code='import json\ndef main(body: str, status_code: int) -> dict:\n if status_code != 200: raise Exception("HTTP_STEP_FAILED_"+str(status_code))\n data=json.loads(body)\n'
if not allow_errors:code+=' if not data.get("success"): raise Exception(data.get("error_code","MODEL_STEP_FAILED"))\n'
code+=' return {'+','.join(repr(k)+': data.get('+repr(k)+','+('""' if typ=='string' else '[]' if typ.startswith('array') else '0')+')' for k,typ in fields.items())+'}\n'
return node(id,'code',title,x,y,code_language='python3',code=code,variables=[{'variable':'body','value_selector':[source,'body']},{'variable':'status_code','value_selector':[source,'status_code']}],outputs={k:{'type':t,'children':None} for k,t in fields.items()})
def end(source,fields,x,y):
return node('end','end','完成:由后台核验并保存',x,y,outputs=[dict(variable=k,value_type=t,value_selector=[source,k]) for k,t in fields.items()])
def app(name,description,nodes,edges):
return dict(kind='app',version='0.7.0',dependencies=[],app=dict(name=name,description=description,mode='workflow',icon='📖',icon_background='#ECFDF3',use_icon_as_answer_icon=False),workflow=dict(conversation_variables=[],environment_variables=[],features={'file_upload':{'enabled':False},'retriever_resource':{'enabled':False},'sensitive_word_avoidance':{'enabled':False},'speech_to_text':{'enabled':False},'text_to_speech':{'enabled':False},'suggested_questions_after_answer':{'enabled':False}},graph=dict(nodes=nodes,edges=edges,viewport=dict(x=0,y=0,zoom=.7))))
nodes=[start(),http('prepare','读取聊天与已有故事','prepare',360,80,kind='extract',prepared=False),parse('prepared','接收任务 / 示例测试','prepare',{'context_id':'string','step_token':'string'},670,80),http('classify','DeepSeek:分类与故事拆分','classify',670,300,prompt=extract_prompt),parse('classified','检查故事拆分结果','classify',{'story_count':'number','daily_candidate_count':'number'},360,300),http('daily','DeepSeek:只审核今天实际事件','daily',50,300,prompt=daily_prompt),parse('today','检查当日原话证据','daily',{'today_actual_count':'number'},50,520),http('dedup','去重:同事件比较新增信息','dedup',360,520),parse('merged','输出新增 / 更新 / 重复计划','dedup',{'new_stories':'number','updated_stories':'number','repeated_stories':'number','daily_count':'number'},670,520)]
fields={'new_stories':'number','updated_stories':'number','repeated_stories':'number','daily_count':'number'};nodes.append(end('merged',fields,670,740));edges=[edge(a['id'],b['id'],nodes) for a,b in zip(nodes,nodes[1:])]
extract=app('聊天整理:当日日常与回忆故事','DeepSeek 分类与事件拆分 → 独立审核今天已发生事项 → 同故事去重与新增信息比较。只记录任务编号和统计;原话、权限检查和故事更新由故事平台负责。',nodes,edges)
(DEST/'chat-to-stories.yml').write_text(json.dumps(extract,ensure_ascii=False,indent=2),encoding='utf-8')
nodes=[start(),http('prepare','读取长者全部可用故事','prepare',360,80,kind='memoir',prepared=False),parse('prepared','接收整本任务 / 示例测试','prepare',{'context_id':'string','step_token':'string'},670,80),http('write-book','Doubao:全部故事统一编写整本','write-book',670,300,prompt=book_prompt),parse('written','检查完整章节与插画计划','write-book',{'chapter_count':'number','chapter_indexes':'array[number]'},360,300),http('validate-book','核验来源与日期 · 先显示正文','validate-book',50,300),parse('validated','所有故事均已成章','validate-book',{'chapter_count':'number'},50,520)]
next(n for n in nodes if n['id']=='write-book')['data']['timeout']['read']=660
iteration=node('pictures','iteration','逐章配图 · 并行 3 章',360,520,iterator_selector=['written','chapter_indexes'],iterator_input_type='array[number]',output_selector=['picture-result','result'],output_type='array[string]',is_parallel=True,parallel_nums=3,error_handle_mode='terminated',start_node_id='pictures-start',width=630,height=235)
iteration['width']=630;iteration['height']=235;iteration['zIndex']=1;nodes.append(iteration)
inner_start=node('pictures-start','iteration-start','',24,80,isInIteration=True);inner_start.update(type='custom-iteration-start',parentId='pictures',width=44,height=48,draggable=False,selectable=False,zIndex=1002)
image=http('illustrate','Seedream:绘制该章情景插画','illustrate',95,65,index=True);image.update(parentId='pictures',extent='parent',zIndex=1002);image['data'].update(isInIteration=True,isInLoop=False,iteration_id='pictures')
image_result=node('picture-result','code','保留正文,记录插画结果',365,65,code_language='python3',code='import json\ndef main(body: str) -> dict:\n data=json.loads(body)\n return {"result":json.dumps(data,ensure_ascii=False)}\n',variables=[{'variable':'body','value_selector':['illustrate','body']}],outputs={'result':{'type':'string','children':None}},isInIteration=True,isInLoop=False,iteration_id='pictures');image_result.update(parentId='pictures',extent='parent',zIndex=1002)
nodes += [inner_start,image,image_result,http('finish','汇总正文与插画完成情况','finish',50,850),parse('finished','结果核验与图片数量','finish',{'chapter_count':'number','illustrations_ready':'number','illustration_errors':'number'},360,850),end('finished',{'chapter_count':'number','illustrations_ready':'number','illustration_errors':'number'},670,850)]
main=['start','prepare','prepared','write-book','written','validate-book','validated','pictures','finish','finished','end'];edges=[edge(a,b,nodes) for a,b in zip(main,main[1:])]+[edge('pictures-start','illustrate',nodes,'pictures'),edge('illustrate','picture-result',nodes,'pictures')]
book=app('图文回忆录:全部故事成书与逐章配图','全部故事一次交给 Doubao-Seed-Evolving 统一成书 → 核验章节、事实与日期 → 正文先显示 → Seedream 并行配图 → 后台检查账号授权和来源版本并保存。',nodes,edges)
(DEST/'stories-to-memoir.yml').write_text(json.dumps(book,ensure_ascii=False,indent=2),encoding='utf-8')
if args.provider_id:
spec = importlib.util.spec_from_file_location('dify_model_tools', ROOT/'scripts/dify-model-tools.py')
tools = importlib.util.module_from_spec(spec)
spec.loader.exec_module(tools)
for workflow, filename in [(extract, 'chat-to-stories.yml'), (book, 'stories-to-memoir.yml')]:
workflow['workflow']['graph'] = tools.convert_graph(workflow['workflow']['graph'], args.provider_id)
(DEST/filename).write_text(json.dumps(workflow,ensure_ascii=False,indent=2),encoding='utf-8')
(DEST.parent/'model-tools.openapi.json').write_text(json.dumps(tools.schema(),ensure_ascii=False,indent=2),encoding='utf-8')
print(json.dumps({'workflows':2,'nodes':[len(extract['workflow']['graph']['nodes']),len(book['workflow']['graph']['nodes'])]}))