120 lines
8.8 KiB
Python
120 lines
8.8 KiB
Python
"""Execute in Dify API Python with PAYLOAD supplied by private host wrapper.
|
|
|
|
Uses deployed services/controllers for credential encryption and app config.
|
|
Never print this process output to public logs: create returns a new app key to
|
|
its private caller; the wrapper writes it 0600 and emits only public metadata.
|
|
"""
|
|
import hashlib
|
|
import inspect
|
|
import json
|
|
import logging
|
|
import sys
|
|
logging.disable(logging.CRITICAL)
|
|
from app_factory import create_app
|
|
app = create_app()
|
|
from sqlalchemy import select
|
|
from extensions.ext_database import db
|
|
from models.account import Account, Tenant, TenantAccountJoin
|
|
from models.model import App, ApiToken
|
|
from models.provider import ProviderModel, ProviderModelCredential, TenantDefaultModel
|
|
from services.app_service import AppService
|
|
from services.model_provider_service import ModelProviderService
|
|
|
|
PROVIDER = 'langgenius/openai_api_compatible/openai_api_compatible'
|
|
ASR = 'Qwen/Qwen3-ASR-1.7B'
|
|
APP_NAME = 'followup-audio-isolated-20261009'
|
|
ENDPOINT = 'http://followup-asr-bridge:8080/v1'
|
|
CONFIG = {
|
|
'model': {'provider': PROVIDER, 'name': 'qwen3.6-35b', 'mode': 'chat',
|
|
'completion_params': {'max_tokens': 8192, 'temperature': 0.0, 'top_p': 1.0, 'enable_thinking': False}},
|
|
'pre_prompt': '仅依据本次用户请求处理提供的文本。遵循用户给定的 JSON 格式,只返回最终 JSON,不输出思考过程、解释或 Markdown。不得把示例当成事实。',
|
|
'prompt_type': 'simple', 'user_input_form': [],
|
|
'speech_to_text': {'enabled': True}, 'text_to_speech': {'enabled': False},
|
|
'suggested_questions_after_answer': {'enabled': False},
|
|
'retriever_resource': {'enabled': False}, 'sensitive_word_avoidance': {'enabled': False},
|
|
'file_upload': {'enabled': False},
|
|
}
|
|
|
|
with app.app_context():
|
|
# Anchor only to the existing default Qwen workspace. Fail if ambiguous.
|
|
tenant_ids = db.session.scalars(select(TenantDefaultModel.tenant_id).where(
|
|
TenantDefaultModel.model_type == 'text-generation', TenantDefaultModel.model_name == 'qwen3.6-35b')).all()
|
|
assert len(tenant_ids) == 1, 'AMBIGUOUS_WORKSPACE'
|
|
tenant_id = tenant_ids[0]
|
|
tenant = db.session.get(Tenant, tenant_id)
|
|
owner = db.session.scalars(select(Account).join(TenantAccountJoin, Account.id == TenantAccountJoin.account_id).where(
|
|
TenantAccountJoin.tenant_id == tenant_id, TenantAccountJoin.role == 'owner')).one()
|
|
owner.current_tenant = tenant
|
|
def current_app_owned():
|
|
resource = PAYLOAD['resources']
|
|
obj = db.session.get(App, resource['app_id'])
|
|
assert obj and obj.name == APP_NAME and obj.tenant_id == tenant_id, 'APP_IDENTITY_MISMATCH'
|
|
return obj
|
|
def public_state(obj):
|
|
model = db.session.scalars(select(ProviderModel).where(ProviderModel.tenant_id == tenant_id,
|
|
ProviderModel.provider_name == PROVIDER, ProviderModel.model_name == ASR, ProviderModel.model_type == 'speech2text')).one()
|
|
default = db.session.scalars(select(TenantDefaultModel).where(TenantDefaultModel.tenant_id == tenant_id,
|
|
TenantDefaultModel.model_type == 'speech2text')).one()
|
|
config = obj.app_model_config.to_dict()
|
|
return {'app_id':obj.id,'app_name':obj.name,'tenant_id':tenant_id,'app_mode':obj.mode,
|
|
'enable_api':obj.enable_api,'enable_site':obj.enable_site,'config_id':obj.app_model_config_id,
|
|
'speech_to_text':obj.app_model_config.speech_to_text_dict,
|
|
'model':obj.app_model_config.model_dict,'pre_prompt_sha256':hashlib.sha256(obj.app_model_config.pre_prompt.encode()).hexdigest(),
|
|
'config_sha256':hashlib.sha256(json.dumps(config,ensure_ascii=False,sort_keys=True,default=str).encode()).hexdigest(),
|
|
'asr_model_id':model.id,'asr_credential_id':model.credential_id,'asr_default_id':default.id,
|
|
'asr_model':model.model_name,'asr_default':default.model_name,'provider':PROVIDER,'endpoint':ENDPOINT,
|
|
'base_url':'https://ai.zhenyangtang.com.cn/v1','max_active_requests':obj.max_active_requests}
|
|
result = {}
|
|
with app.test_request_context('/', method='POST', json=CONFIG):
|
|
app.login_manager._update_request_context_with_user(owner)
|
|
svc = AppService()
|
|
action = PAYLOAD['action']
|
|
if action == 'create':
|
|
assert not db.session.scalar(select(App.id).where(App.name == APP_NAME)), 'APP_NAME_ALREADY_EXISTS'
|
|
assert not db.session.scalar(select(ProviderModel.id).where(ProviderModel.tenant_id == tenant_id,ProviderModel.model_type == 'speech2text')), 'SPEECH2TEXT_NOT_EMPTY'
|
|
assert not db.session.scalar(select(TenantDefaultModel.id).where(TenantDefaultModel.tenant_id == tenant_id,TenantDefaultModel.model_type == 'speech2text')), 'DEFAULT_NOT_EMPTY'
|
|
ModelProviderService().create_model_credential(tenant_id,PROVIDER,'speech2text',ASR,
|
|
{'api_key':PAYLOAD['asr_key'],'endpoint_url':ENDPOINT,'endpoint_model_name':ASR,'language':'zh','initial_prompt':'','display_name':'Followup ASR isolated'},
|
|
'followup-asr-isolated-20261009')
|
|
ModelProviderService().update_default_model_of_model_type(tenant_id,'speech2text',PROVIDER,ASR)
|
|
obj=svc.create_app(tenant_id,{'mode':'chat','name':APP_NAME,'description':'Isolated synthetic-verified ASR and JSON extraction. Preview only; no clinical writes.','icon_type':'emoji','icon':'🎙️','icon_background':'#E4FBCC'},owner)
|
|
svc.update_app_site_status(obj,False)
|
|
from controllers.console.app.model_config import ModelConfigResource
|
|
inspect.unwrap(ModelConfigResource.post)(ModelConfigResource(),obj)
|
|
svc.update_app(obj, {'name':APP_NAME,'description':obj.description,'icon_type':obj.icon_type,'icon':obj.icon,'icon_background':obj.icon_background,'use_icon_as_answer_icon':False,'max_active_requests':1})
|
|
svc.update_app_api_status(obj,True)
|
|
from controllers.console.apikey import AppApiKeyListResource, BaseApiKeyListResource
|
|
token,status=inspect.unwrap(BaseApiKeyListResource.post)(AppApiKeyListResource(),obj.id)
|
|
assert status == 201
|
|
result={'public':public_state(obj),'private':{'api_key':token.token,'key_id':token.id}}
|
|
elif action in ('status','disable','enable'):
|
|
obj=current_app_owned()
|
|
if action != 'status':svc.update_app_api_status(obj,action == 'enable')
|
|
result={'public':public_state(obj)}
|
|
elif action == 'remove':
|
|
resource=PAYLOAD['resources'];obj=current_app_owned()
|
|
credential=db.session.get(ProviderModelCredential,resource['asr_credential_id'])
|
|
recorded_default=db.session.get(TenantDefaultModel,resource['asr_default_id'])
|
|
assert credential and credential.tenant_id==tenant_id and credential.model_name==ASR and credential.model_type=='speech2text', 'CREDENTIAL_IDENTITY_MISMATCH'
|
|
assert recorded_default and recorded_default.tenant_id==tenant_id and recorded_default.model_name==ASR and recorded_default.model_type=='speech2text', 'DEFAULT_IDENTITY_MISMATCH'
|
|
for other in db.session.scalars(select(App).where(App.tenant_id==tenant_id,App.id!=obj.id)).all():
|
|
features=(other.workflow.features_dict if other.workflow else {}) if other.mode in ('advanced-chat','workflow') else ({'speech_to_text':other.app_model_config.speech_to_text_dict} if other.app_model_config else {})
|
|
assert not features.get('speech_to_text',{}).get('enabled'), 'ANOTHER_APP_NOW_USES_ASR'
|
|
svc.update_app_api_status(obj,False)
|
|
# Only the precise new app key, via the deployed API controller.
|
|
from controllers.console.apikey import AppApiKeyResource, BaseApiKeyResource
|
|
key_id=resource['key_id']
|
|
if db.session.get(ApiToken,key_id):
|
|
inspect.unwrap(BaseApiKeyResource.delete)(AppApiKeyResource(),obj.id,key_id)
|
|
svc.delete_app(obj)
|
|
default=db.session.get(TenantDefaultModel,resource['asr_default_id'])
|
|
assert default and default.tenant_id==tenant_id and default.model_type=='speech2text' and default.model_name==ASR, 'DEFAULT_IDENTITY_MISMATCH'
|
|
# No service offers reset-to-empty; remove only this recorded, new, non-secret default row.
|
|
db.session.delete(default);db.session.commit()
|
|
ModelProviderService().remove_model_credential(tenant_id,PROVIDER,'speech2text',ASR,resource['asr_credential_id'])
|
|
assert not db.session.scalar(select(App.id).where(App.id==resource['app_id']))
|
|
assert not db.session.scalar(select(ProviderModel.id).where(ProviderModel.id==resource['asr_model_id']))
|
|
result={'public':{'removed_app_id':resource['app_id'],'speech2text_restored_empty':True}}
|
|
else:raise ValueError('UNKNOWN_ACTION')
|
|
print('OPERATOR_RESULT='+json.dumps(result,ensure_ascii=False,default=str))
|