chore: format
This commit is contained in:
1 parent
1f49ebf431
commit
6fce92aa12
139 files changed
+92569
-3249
No files matched your search
@@ -72,7 +72,6 @@ SPEECH_CACHE_DIR = CACHE_DIR / 'audio' / 'speech'
|
||||
SPEECH_CACHE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
|
||||
def is_audio_conversion_required(file_path):
|
||||
"""
|
||||
Check if the given audio file needs conversion to mp3.
|
||||
@@ -307,8 +306,6 @@ async def update_audio_config(request: Request, form_data: AudioConfigUpdateForm
|
||||
else:
|
||||
request.app.state.faster_whisper_model = None
|
||||
|
||||
|
||||
|
||||
return {
|
||||
'tts': {
|
||||
'ENGINE': request.app.state.config.TTS_ENGINE,
|
||||
@@ -377,7 +374,10 @@ async def _raise_tts_error(exc: Exception, r=None) -> None:
|
||||
|
||||
|
||||
async def _write_tts_cache(
|
||||
file_path: Path, audio: bytes, body_path: Path, payload: dict,
|
||||
file_path: Path,
|
||||
audio: bytes,
|
||||
body_path: Path,
|
||||
payload: dict,
|
||||
) -> None:
|
||||
"""Persist audio + request metadata to the speech cache."""
|
||||
async with aiofiles.open(file_path, 'wb') as f:
|
||||
@@ -405,16 +405,16 @@ async def _tts_openai(request, payload, file_path, file_body_path, user):
|
||||
session = await get_session()
|
||||
r = await session.post(
|
||||
url=f'{request.app.state.config.TTS_OPENAI_API_BASE_URL}/audio/speech',
|
||||
json=payload, headers=headers, ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
json=payload,
|
||||
headers=headers,
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
)
|
||||
r.raise_for_status()
|
||||
|
||||
audio_data = await r.read()
|
||||
content_type = r.headers.get('Content-Type', 'audio/mpeg')
|
||||
|
||||
if not await asyncio.to_thread(
|
||||
transcode_audio_to_mp3, audio_data, content_type, file_path
|
||||
):
|
||||
if not await asyncio.to_thread(transcode_audio_to_mp3, audio_data, content_type, file_path):
|
||||
async with aiofiles.open(file_path, 'wb') as f:
|
||||
await f.write(audio_data)
|
||||
|
||||
@@ -595,9 +595,7 @@ async def speech(request: Request, user=Depends(get_verified_user)):
|
||||
|
||||
body = await request.body()
|
||||
name = hashlib.sha256(
|
||||
body
|
||||
+ str(engine).encode('utf-8')
|
||||
+ str(request.app.state.config.TTS_MODEL).encode('utf-8')
|
||||
body + str(engine).encode('utf-8') + str(request.app.state.config.TTS_MODEL).encode('utf-8')
|
||||
).hexdigest()
|
||||
|
||||
file_path = SPEECH_CACHE_DIR.joinpath(f'{name}.mp3')
|
||||
@@ -628,8 +626,11 @@ async def _transcribe_whisper(request, file_path, languages, file_dir, id):
|
||||
|
||||
def _run():
|
||||
segments, info = model.transcribe(
|
||||
file_path, beam_size=5, vad_filter=WHISPER_VAD_FILTER,
|
||||
language=languages[0], multilingual=WHISPER_MULTILINGUAL,
|
||||
file_path,
|
||||
beam_size=5,
|
||||
vad_filter=WHISPER_VAD_FILTER,
|
||||
language=languages[0],
|
||||
multilingual=WHISPER_MULTILINGUAL,
|
||||
)
|
||||
log.info("Detected language '%s' with probability %f" % (info.language, info.language_probability))
|
||||
return ''.join([segment.text for segment in list(segments)])
|
||||
@@ -665,7 +666,9 @@ async def _transcribe_openai(request, file_path, filename, languages, file_dir,
|
||||
|
||||
r = await session.post(
|
||||
url=f'{request.app.state.config.STT_OPENAI_API_BASE_URL}/audio/transcriptions',
|
||||
headers=headers, data=form_data, ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
headers=headers,
|
||||
data=form_data,
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
)
|
||||
if r.status == 200:
|
||||
break
|
||||
@@ -724,11 +727,7 @@ async def _transcribe_deepgram(request, file_path, languages, file_dir, id):
|
||||
|
||||
# Parse the Deepgram response structure
|
||||
try:
|
||||
transcript = (
|
||||
body['results']['channels'][0]['alternatives'][0]
|
||||
.get('transcript', '')
|
||||
.strip()
|
||||
)
|
||||
transcript = body['results']['channels'][0]['alternatives'][0].get('transcript', '').strip()
|
||||
except (KeyError, IndexError) as exc:
|
||||
log.error(f'Malformed Deepgram response: {exc}')
|
||||
raise Exception('Failed to parse Deepgram response') from exc
|
||||
@@ -744,7 +743,11 @@ async def _transcribe_deepgram(request, file_path, languages, file_dir, id):
|
||||
if r is not None:
|
||||
try:
|
||||
res = await r.json()
|
||||
msg = res.get('error', {}).get('message', '') if isinstance(res.get('error'), dict) else str(res.get('error', ''))
|
||||
msg = (
|
||||
res.get('error', {}).get('message', '')
|
||||
if isinstance(res.get('error'), dict)
|
||||
else str(res.get('error', ''))
|
||||
)
|
||||
if msg:
|
||||
detail = f'External: {msg}'
|
||||
except Exception:
|
||||
@@ -752,7 +755,6 @@ async def _transcribe_deepgram(request, file_path, languages, file_dir, id):
|
||||
raise Exception(detail)
|
||||
|
||||
|
||||
|
||||
async def _transcribe_azure(request, file_path, filename, file_dir, id):
|
||||
"""Transcribe audio via Azure Cognitive Services batch transcription."""
|
||||
if not os.path.isfile(file_path):
|
||||
@@ -762,7 +764,7 @@ async def _transcribe_azure(request, file_path, filename, file_dir, id):
|
||||
if audio_size > AZURE_MAX_FILE_SIZE:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail=f'File size ({audio_size // (1024*1024)}MB) exceeds Azure limit of {AZURE_MAX_FILE_SIZE_MB}MB',
|
||||
detail=f'File size ({audio_size // (1024 * 1024)}MB) exceeds Azure limit of {AZURE_MAX_FILE_SIZE_MB}MB',
|
||||
)
|
||||
|
||||
api_key = request.app.state.config.AUDIO_STT_AZURE_API_KEY
|
||||
@@ -773,11 +775,23 @@ async def _transcribe_azure(request, file_path, filename, file_dir, id):
|
||||
|
||||
# Default to a broad set of locales when none are configured
|
||||
if len(locale_str) < 2:
|
||||
locale_str = ','.join([
|
||||
'en-US', 'es-ES', 'es-MX', 'fr-FR', 'hi-IN',
|
||||
'it-IT', 'de-DE', 'en-GB', 'en-IN', 'ja-JP',
|
||||
'ko-KR', 'pt-BR', 'zh-CN',
|
||||
])
|
||||
locale_str = ','.join(
|
||||
[
|
||||
'en-US',
|
||||
'es-ES',
|
||||
'es-MX',
|
||||
'fr-FR',
|
||||
'hi-IN',
|
||||
'it-IT',
|
||||
'de-DE',
|
||||
'en-GB',
|
||||
'en-IN',
|
||||
'ja-JP',
|
||||
'ko-KR',
|
||||
'pt-BR',
|
||||
'zh-CN',
|
||||
]
|
||||
)
|
||||
|
||||
if not api_key or not region:
|
||||
raise HTTPException(status_code=400, detail='Azure API key and region are required for Azure STT')
|
||||
@@ -785,7 +799,8 @@ async def _transcribe_azure(request, file_path, filename, file_dir, id):
|
||||
# Build the transcription definition payload
|
||||
definition = json.dumps(
|
||||
{'locales': locale_str.split(','), 'diarization': {'maxSpeakers': max_speakers, 'enabled': True}}
|
||||
if locale_str else {}
|
||||
if locale_str
|
||||
else {}
|
||||
)
|
||||
endpoint = (
|
||||
base_url or f'https://{region}.api.cognitive.microsoft.com'
|
||||
@@ -799,7 +814,8 @@ async def _transcribe_azure(request, file_path, filename, file_dir, id):
|
||||
try:
|
||||
session = await get_session()
|
||||
r = await session.post(
|
||||
url=endpoint, data=form_data,
|
||||
url=endpoint,
|
||||
data=form_data,
|
||||
headers={'Ocp-Apim-Subscription-Key': api_key},
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
)
|
||||
@@ -833,8 +849,10 @@ async def _transcribe_azure(request, file_path, filename, file_dir, id):
|
||||
if 'code' in res and 'message' in res:
|
||||
azure_code = res.get('innerError', {}).get('code', res['code'])
|
||||
user_facing_codes = {
|
||||
'EmptyAudioFile', 'AudioLengthLimitExceeded',
|
||||
'NoLanguageIdentified', 'MultipleLanguagesIdentified',
|
||||
'EmptyAudioFile',
|
||||
'AudioLengthLimitExceeded',
|
||||
'NoLanguageIdentified',
|
||||
'MultipleLanguagesIdentified',
|
||||
}
|
||||
if azure_code in user_facing_codes:
|
||||
detail = res['message']
|
||||
@@ -851,7 +869,6 @@ async def _transcribe_azure(request, file_path, filename, file_dir, id):
|
||||
)
|
||||
|
||||
|
||||
|
||||
async def transcription_handler(request, file_path, metadata, user=None):
|
||||
filename = os.path.basename(file_path)
|
||||
file_dir = os.path.dirname(file_path)
|
||||
@@ -897,8 +914,7 @@ async def _transcribe_mistral(request, file_path, filename, metadata, file_dir,
|
||||
try:
|
||||
model = request.app.state.config.STT_MODEL or 'voxtral-mini-latest'
|
||||
log.info(
|
||||
f'Mistral STT - model: {model}, '
|
||||
f'method: {"chat_completions" if use_chat_completions else "transcriptions"}'
|
||||
f'Mistral STT - model: {model}, method: {"chat_completions" if use_chat_completions else "transcriptions"}'
|
||||
)
|
||||
|
||||
session = await get_session()
|
||||
@@ -932,17 +948,20 @@ async def _transcribe_mistral(request, file_path, filename, metadata, file_dir,
|
||||
|
||||
payload = {
|
||||
'model': model,
|
||||
'messages': [{
|
||||
'role': 'user',
|
||||
'content': [
|
||||
{'type': 'input_audio', 'input_audio': audio_base64},
|
||||
{'type': 'text', 'text': text_instruction},
|
||||
],
|
||||
}],
|
||||
'messages': [
|
||||
{
|
||||
'role': 'user',
|
||||
'content': [
|
||||
{'type': 'input_audio', 'input_audio': audio_base64},
|
||||
{'type': 'text', 'text': text_instruction},
|
||||
],
|
||||
}
|
||||
],
|
||||
}
|
||||
|
||||
r = await session.post(
|
||||
url=f'{api_base_url}/chat/completions', json=payload,
|
||||
url=f'{api_base_url}/chat/completions',
|
||||
json=payload,
|
||||
headers={'Authorization': f'Bearer {api_key}', 'Content-Type': 'application/json'},
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
)
|
||||
@@ -969,7 +988,8 @@ async def _transcribe_mistral(request, file_path, filename, metadata, file_dir,
|
||||
form_data.add_field('file', open(file_path, 'rb'), filename=filename, content_type=mime_type)
|
||||
|
||||
r = await session.post(
|
||||
url=f'{api_base_url}/audio/transcriptions', data=form_data,
|
||||
url=f'{api_base_url}/audio/transcriptions',
|
||||
data=form_data,
|
||||
headers={'Authorization': f'Bearer {api_key}'},
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
)
|
||||
@@ -1041,10 +1061,7 @@ async def transcribe(request: Request, file_path: str, metadata: Optional[dict]
|
||||
|
||||
results = []
|
||||
try:
|
||||
tasks = [
|
||||
transcription_handler(request, chunk_path, metadata, user)
|
||||
for chunk_path in chunk_paths
|
||||
]
|
||||
tasks = [transcription_handler(request, chunk_path, metadata, user) for chunk_path in chunk_paths]
|
||||
for coro in asyncio.as_completed(tasks):
|
||||
try:
|
||||
results.append(await coro)
|
||||
@@ -1226,7 +1243,8 @@ async def get_available_models(request: Request) -> list[dict]:
|
||||
try:
|
||||
async with session.get(
|
||||
f'{base_url}/audio/models',
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL, timeout=_timeout,
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
timeout=_timeout,
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
data = await resp.json()
|
||||
@@ -1236,7 +1254,8 @@ async def get_available_models(request: Request) -> list[dict]:
|
||||
try:
|
||||
async with session.get(
|
||||
f'{base_url}/models',
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL, timeout=_timeout,
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
timeout=_timeout,
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
data = await resp.json()
|
||||
@@ -1256,7 +1275,8 @@ async def get_available_models(request: Request) -> list[dict]:
|
||||
'xi-api-key': request.app.state.config.TTS_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL, timeout=_timeout,
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
timeout=_timeout,
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
models = await resp.json()
|
||||
@@ -1274,9 +1294,14 @@ async def get_available_models(request: Request) -> list[dict]:
|
||||
async def get_models(request: Request, user=Depends(get_verified_user)):
|
||||
return {'models': await get_available_models(request)}
|
||||
|
||||
|
||||
_OPENAI_DEFAULT_VOICES = {
|
||||
'alloy': 'alloy', 'echo': 'echo', 'fable': 'fable',
|
||||
'onyx': 'onyx', 'nova': 'nova', 'shimmer': 'shimmer',
|
||||
'alloy': 'alloy',
|
||||
'echo': 'echo',
|
||||
'fable': 'fable',
|
||||
'onyx': 'onyx',
|
||||
'nova': 'nova',
|
||||
'shimmer': 'shimmer',
|
||||
}
|
||||
|
||||
|
||||
@@ -1292,7 +1317,8 @@ async def get_available_voices(request) -> dict:
|
||||
session = await get_session()
|
||||
async with session.get(
|
||||
f'{base_url}/audio/voices',
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL, timeout=_timeout,
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
timeout=_timeout,
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
data = await resp.json()
|
||||
@@ -1311,7 +1337,8 @@ async def get_available_voices(request) -> dict:
|
||||
'xi-api-key': request.app.state.config.TTS_API_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL, timeout=_timeout,
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
timeout=_timeout,
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
voices_data = await resp.json()
|
||||
@@ -1330,7 +1357,8 @@ async def get_available_voices(request) -> dict:
|
||||
async with session.get(
|
||||
url,
|
||||
headers={'Ocp-Apim-Subscription-Key': request.app.state.config.TTS_API_KEY},
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL, timeout=_timeout,
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
timeout=_timeout,
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
voices = await resp.json()
|
||||
@@ -1348,7 +1376,8 @@ async def get_available_voices(request) -> dict:
|
||||
async with session.get(
|
||||
f'{api_base_url}/audio/voices',
|
||||
headers={'Authorization': f'Bearer {api_key}'},
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL, timeout=_timeout,
|
||||
ssl=AIOHTTP_CLIENT_SESSION_SSL,
|
||||
timeout=_timeout,
|
||||
) as resp:
|
||||
resp.raise_for_status()
|
||||
voices_data = await resp.json()
|
||||
|
||||
@@ -514,9 +514,13 @@ async def delete_all_user_chats(
|
||||
|
||||
@router.get('/list/user/{user_id}', response_model=list[ChatTitleIdResponse])
|
||||
async def get_user_chat_list_by_user_id(
|
||||
user_id: str, page: int | None = None, query: str | None = None,
|
||||
order_by: str | None = None, direction: str | None = None,
|
||||
user=Depends(get_admin_user), db: AsyncSession = Depends(get_async_session),
|
||||
user_id: str,
|
||||
page: int | None = None,
|
||||
query: str | None = None,
|
||||
order_by: str | None = None,
|
||||
direction: str | None = None,
|
||||
user=Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
"""List chat summaries for a given user (admin-only endpoint)."""
|
||||
if not ENABLE_ADMIN_CHAT_ACCESS:
|
||||
|
||||
@@ -123,9 +123,7 @@ async def process_uploaded_file(
|
||||
if _is_text_file(file_path):
|
||||
content_type = 'text/plain'
|
||||
|
||||
stt_supported = getattr(
|
||||
request.app.state.config, 'STT_SUPPORTED_CONTENT_TYPES', []
|
||||
)
|
||||
stt_supported = getattr(request.app.state.config, 'STT_SUPPORTED_CONTENT_TYPES', [])
|
||||
|
||||
if content_type and strict_match_mime_type(stt_supported, content_type):
|
||||
# Audio / STT-supported files → transcribe then index
|
||||
@@ -161,17 +159,12 @@ async def process_uploaded_file(
|
||||
db=db_session,
|
||||
)
|
||||
else:
|
||||
raise Exception(
|
||||
f'File type {content_type} is not supported for processing'
|
||||
)
|
||||
raise Exception(f'File type {content_type} is not supported for processing')
|
||||
|
||||
else:
|
||||
# Documents, or any file when an external engine is configured
|
||||
if not content_type:
|
||||
log.info(
|
||||
f'File type {file.content_type} is not provided, '
|
||||
'but trying to process anyway'
|
||||
)
|
||||
log.info(f'File type {file.content_type} is not provided, but trying to process anyway')
|
||||
await process_file(
|
||||
request,
|
||||
ProcessFileForm(file_id=file_item.id),
|
||||
|
||||
@@ -336,29 +336,16 @@ async def preview_group_access(
|
||||
return {
|
||||
'group': {'id': group.id, 'name': group.name},
|
||||
'models': {
|
||||
'items': [
|
||||
{'id': m.id, 'name': m.name}
|
||||
for m in active_models
|
||||
if m.id in accessible_model_ids
|
||||
],
|
||||
'items': [{'id': m.id, 'name': m.name} for m in active_models if m.id in accessible_model_ids],
|
||||
'total': len(active_models),
|
||||
},
|
||||
'knowledge': {
|
||||
'items': [
|
||||
{'id': k.id, 'name': k.name}
|
||||
for k in all_knowledge
|
||||
if k.id in accessible_knowledge_ids
|
||||
],
|
||||
'items': [{'id': k.id, 'name': k.name} for k in all_knowledge if k.id in accessible_knowledge_ids],
|
||||
'total': len(all_knowledge),
|
||||
},
|
||||
'tools': {
|
||||
'items': [
|
||||
{'id': t.id, 'name': t.name}
|
||||
for t in all_tools
|
||||
if t.id in accessible_tool_ids
|
||||
],
|
||||
'items': [{'id': t.id, 'name': t.name} for t in all_tools if t.id in accessible_tool_ids],
|
||||
'total': len(all_tools),
|
||||
},
|
||||
'permissions': group.permissions or {},
|
||||
}
|
||||
|
||||
@@ -447,6 +447,7 @@ def _is_same_origin(url: str, base_url: str) -> bool:
|
||||
and comparing the three origin components eliminates those
|
||||
attack vectors.
|
||||
"""
|
||||
|
||||
def _default_port(scheme: str) -> int:
|
||||
return 443 if scheme == 'https' else 80
|
||||
|
||||
@@ -455,8 +456,7 @@ def _is_same_origin(url: str, base_url: str) -> bool:
|
||||
return (
|
||||
parsed.scheme == trusted.scheme
|
||||
and parsed.hostname == trusted.hostname
|
||||
and (parsed.port or _default_port(parsed.scheme))
|
||||
== (trusted.port or _default_port(trusted.scheme))
|
||||
and (parsed.port or _default_port(parsed.scheme)) == (trusted.port or _default_port(trusted.scheme))
|
||||
)
|
||||
|
||||
|
||||
@@ -743,7 +743,8 @@ async def image_generations(
|
||||
headers = {'Authorization': f'Bearer {request.app.state.config.COMFYUI_API_KEY}'}
|
||||
|
||||
image_data, content_type = await get_image_data(
|
||||
image['url'], headers,
|
||||
image['url'],
|
||||
headers,
|
||||
trusted_base_url=request.app.state.config.COMFYUI_BASE_URL,
|
||||
)
|
||||
_, url = await upload_image(
|
||||
@@ -1102,7 +1103,8 @@ async def image_edits(
|
||||
headers = {'Authorization': f'Bearer {request.app.state.config.IMAGES_EDIT_COMFYUI_API_KEY}'}
|
||||
|
||||
image_data, content_type = await get_image_data(
|
||||
image_url, headers,
|
||||
image_url,
|
||||
headers,
|
||||
trusted_base_url=request.app.state.config.IMAGES_EDIT_COMFYUI_BASE_URL,
|
||||
)
|
||||
_, url = await upload_image(
|
||||
|
||||
@@ -1336,8 +1336,11 @@ async def add_files_to_knowledge_batch(
|
||||
dir_map = {form.file_id: form.directory_id for form in new_entries}
|
||||
for file_id in successful_file_ids:
|
||||
await Knowledges.add_file_to_knowledge_by_id(
|
||||
knowledge_id=id, file_id=file_id, user_id=user.id,
|
||||
directory_id=dir_map.get(file_id), db=db,
|
||||
knowledge_id=id,
|
||||
file_id=file_id,
|
||||
user_id=user.id,
|
||||
directory_id=dir_map.get(file_id),
|
||||
db=db,
|
||||
)
|
||||
|
||||
# If there were any errors, include them in the response
|
||||
|
||||
@@ -63,7 +63,9 @@ class MemoryUpdateModel(BaseModel):
|
||||
|
||||
@router.post('/add', response_model=MemoryModel | None)
|
||||
async def add_memory(
|
||||
request: Request, form_data: AddMemoryForm, user=Depends(get_verified_user),
|
||||
request: Request,
|
||||
form_data: AddMemoryForm,
|
||||
user=Depends(get_verified_user),
|
||||
):
|
||||
"""Persist a new memory and embed it into the user's vector collection.
|
||||
|
||||
|
||||
@@ -220,8 +220,10 @@ async def get_model_tags(user=Depends(get_verified_user), db: AsyncSession = Dep
|
||||
|
||||
@router.post('/create', response_model=ModelModel | None)
|
||||
async def create_new_model(
|
||||
request: Request, form_data: ModelForm,
|
||||
user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session),
|
||||
request: Request,
|
||||
form_data: ModelForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
"""Create a new workspace model entry."""
|
||||
if user.role != 'admin' and not await has_permission(
|
||||
@@ -248,7 +250,8 @@ async def create_new_model(
|
||||
else:
|
||||
await _verify_knowledge_file_access(
|
||||
getattr(form_data.meta, 'knowledge', None) if form_data.meta else None,
|
||||
user, db,
|
||||
user,
|
||||
db,
|
||||
)
|
||||
|
||||
form_data.access_grants = await filter_allowed_access_grants(
|
||||
@@ -361,13 +364,14 @@ async def import_models(
|
||||
try:
|
||||
await _verify_knowledge_file_access(
|
||||
(model_data.get('meta') or {}).get('knowledge'),
|
||||
user, db,
|
||||
user,
|
||||
db,
|
||||
)
|
||||
except HTTPException:
|
||||
log.warning(
|
||||
'import_models: user %s skipped model %s '
|
||||
'(knowledge file access denied)',
|
||||
user.id, model_id,
|
||||
'import_models: user %s skipped model %s (knowledge file access denied)',
|
||||
user.id,
|
||||
model_id,
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -634,14 +638,17 @@ async def toggle_model_by_id(id: str, user=Depends(get_verified_user), db: Async
|
||||
|
||||
@router.post('/model/update', response_model=ModelModel | None)
|
||||
async def update_model_by_id(
|
||||
request: Request, form_data: ModelForm,
|
||||
user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session),
|
||||
request: Request,
|
||||
form_data: ModelForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
"""Update a workspace model's configuration."""
|
||||
model = await Models.get_model_by_id(form_data.id, db=db)
|
||||
if not model:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
if (
|
||||
@@ -662,7 +669,8 @@ async def update_model_by_id(
|
||||
|
||||
await _verify_knowledge_file_access(
|
||||
getattr(form_data.meta, 'knowledge', None) if form_data.meta else None,
|
||||
user, db,
|
||||
user,
|
||||
db,
|
||||
)
|
||||
|
||||
form_data.access_grants = await filter_allowed_access_grants(
|
||||
|
||||
@@ -338,9 +338,7 @@ async def get_all_models(request: Request, user: UserModel | None = None):
|
||||
allowed_model_ids = api_config.get('model_ids', [])
|
||||
|
||||
if allowed_model_ids and 'models' in response:
|
||||
response['models'] = [
|
||||
m for m in response['models'] if m['model'] in allowed_model_ids
|
||||
]
|
||||
response['models'] = [m for m in response['models'] if m['model'] in allowed_model_ids]
|
||||
|
||||
for m in response.get('models', []):
|
||||
if prefix_id:
|
||||
@@ -350,11 +348,7 @@ async def get_all_models(request: Request, user: UserModel | None = None):
|
||||
if connection_type:
|
||||
m['connection_type'] = connection_type
|
||||
|
||||
models_dict = {
|
||||
'models': merge_models_lists(
|
||||
r.get('models', []) if r else None for r in responses
|
||||
)
|
||||
}
|
||||
models_dict = {'models': merge_models_lists(r.get('models', []) if r else None for r in responses)}
|
||||
|
||||
# Annotate with expiry info from loaded-model state
|
||||
try:
|
||||
@@ -386,7 +380,8 @@ async def get_filtered_models(models, user, db=None):
|
||||
db=db,
|
||||
)
|
||||
return [
|
||||
m for m in models.get('models', [])
|
||||
m
|
||||
for m in models.get('models', [])
|
||||
if (mi := model_infos.get(m['model'])) and (user.id == mi.user_id or mi.id in accessible_ids)
|
||||
]
|
||||
|
||||
@@ -439,19 +434,13 @@ async def get_ollama_loaded_models(
|
||||
for idx, response in enumerate(responses):
|
||||
if not response:
|
||||
continue
|
||||
api_config = _resolve_api_config(
|
||||
request.app.state.config, idx, request.app.state.config.OLLAMA_BASE_URLS[idx]
|
||||
)
|
||||
api_config = _resolve_api_config(request.app.state.config, idx, request.app.state.config.OLLAMA_BASE_URLS[idx])
|
||||
prefix_id = api_config.get('prefix_id')
|
||||
if prefix_id:
|
||||
for m in response.get('models', []):
|
||||
m['model'] = f'{prefix_id}.{m["model"]}'
|
||||
|
||||
return {
|
||||
'models': merge_models_lists(
|
||||
r.get('models', []) if r else None for r in responses
|
||||
)
|
||||
}
|
||||
return {'models': merge_models_lists(r.get('models', []) if r else None for r in responses)}
|
||||
|
||||
|
||||
@router.get('/api/version')
|
||||
@@ -472,7 +461,8 @@ async def get_ollama_versions(
|
||||
tasks = []
|
||||
for idx, url in enumerate(request.app.state.config.OLLAMA_BASE_URLS):
|
||||
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||
str(idx), request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}),
|
||||
str(idx),
|
||||
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}),
|
||||
)
|
||||
if api_config.get('enable', True):
|
||||
tasks.append(send_get_request(f'{url}/api/version', api_config.get('key')))
|
||||
@@ -799,7 +789,8 @@ async def embed(
|
||||
|
||||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||
str(url_idx), request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}),
|
||||
str(url_idx),
|
||||
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}),
|
||||
)
|
||||
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
|
||||
|
||||
@@ -852,7 +843,8 @@ async def embeddings(
|
||||
|
||||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||
str(url_idx), request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}),
|
||||
str(url_idx),
|
||||
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}),
|
||||
)
|
||||
key = get_api_key(url_idx, url, request.app.state.config.OLLAMA_API_CONFIGS)
|
||||
|
||||
@@ -910,7 +902,8 @@ async def generate_completion(
|
||||
|
||||
url = request.app.state.config.OLLAMA_BASE_URLS[url_idx]
|
||||
api_config = request.app.state.config.OLLAMA_API_CONFIGS.get(
|
||||
str(url_idx), request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}),
|
||||
str(url_idx),
|
||||
request.app.state.config.OLLAMA_API_CONFIGS.get(url, {}),
|
||||
)
|
||||
|
||||
prefix_id = api_config.get('prefix_id')
|
||||
@@ -1027,9 +1020,7 @@ async def generate_chat_completion(
|
||||
|
||||
if model_info is not None:
|
||||
if model_info.base_model_id:
|
||||
base_model_id = (
|
||||
request.base_model_id if hasattr(request, 'base_model_id') else model_info.base_model_id
|
||||
)
|
||||
base_model_id = request.base_model_id if hasattr(request, 'base_model_id') else model_info.base_model_id
|
||||
payload['model'] = base_model_id
|
||||
|
||||
params = model_info.params.model_dump()
|
||||
@@ -1331,19 +1322,12 @@ async def get_openai_models(
|
||||
raw_models = model_list.get('models', [])
|
||||
|
||||
now_ts = int(time.time())
|
||||
models = [
|
||||
{'id': m['model'], 'object': 'model', 'created': now_ts, 'owned_by': 'openai'}
|
||||
for m in raw_models
|
||||
]
|
||||
models = [{'id': m['model'], 'object': 'model', 'created': now_ts, 'owned_by': 'openai'} for m in raw_models]
|
||||
|
||||
if user.role == 'user' and not BYPASS_MODEL_ACCESS_CONTROL:
|
||||
model_ids = [m['id'] for m in models]
|
||||
model_infos = {
|
||||
mi.id: mi for mi in await Models.get_models_by_ids(model_ids, db=db)
|
||||
}
|
||||
user_group_ids = {
|
||||
g.id for g in await Groups.get_groups_by_member_id(user.id, db=db)
|
||||
}
|
||||
model_infos = {mi.id: mi for mi in await Models.get_models_by_ids(model_ids, db=db)}
|
||||
user_group_ids = {g.id for g in await Groups.get_groups_by_member_id(user.id, db=db)}
|
||||
accessible_ids = await AccessGrants.get_accessible_resource_ids(
|
||||
user_id=user.id,
|
||||
resource_type='model',
|
||||
@@ -1353,8 +1337,7 @@ async def get_openai_models(
|
||||
db=db,
|
||||
)
|
||||
models = [
|
||||
m for m in models
|
||||
if (mi := model_infos.get(m['id'])) and (user.id == mi.user_id or mi.id in accessible_ids)
|
||||
m for m in models if (mi := model_infos.get(m['id'])) and (user.id == mi.user_id or mi.id in accessible_ids)
|
||||
]
|
||||
|
||||
return {'data': models, 'object': 'list'}
|
||||
|
||||
@@ -239,8 +239,11 @@ async def get_prompt_by_id(
|
||||
|
||||
@router.post('/id/{prompt_id}/update', response_model=PromptModel | None)
|
||||
async def update_prompt_by_id(
|
||||
request: Request, prompt_id: str, form_data: PromptForm,
|
||||
user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session),
|
||||
request: Request,
|
||||
prompt_id: str,
|
||||
form_data: PromptForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
"""Update a prompt's content, creating a new history entry if changed."""
|
||||
prompt = await Prompts.get_prompt_by_id(prompt_id, db=db)
|
||||
|
||||
@@ -1314,7 +1314,7 @@ def merge_docs_to_target_size(
|
||||
prev = result[-1]
|
||||
if not can_merge_chunks(prev, chunk):
|
||||
return False
|
||||
merged = f"{prev.page_content}\n\n{content}"
|
||||
merged = f'{prev.page_content}\n\n{content}'
|
||||
if measure(merged) > max_size:
|
||||
return False
|
||||
result[-1] = Document(page_content=merged, metadata={**prev.metadata})
|
||||
@@ -2211,7 +2211,6 @@ async def search_web(request: Request, engine: str, query: str, user=None) -> li
|
||||
raise Exception('No search engine API key found in environment variables')
|
||||
|
||||
|
||||
|
||||
@router.post('/process/web/search')
|
||||
async def process_web_search(request: Request, form_data: SearchForm, user=Depends(get_verified_user)):
|
||||
if not request.app.state.config.ENABLE_WEB_SEARCH:
|
||||
|
||||
@@ -324,8 +324,10 @@ async def export_tools(
|
||||
|
||||
@router.post('/create', response_model=ToolResponse | None)
|
||||
async def create_new_tools(
|
||||
request: Request, form_data: ToolForm,
|
||||
user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session),
|
||||
request: Request,
|
||||
form_data: ToolForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
"""Create a new tool from user-supplied Python source code."""
|
||||
if user.role != 'admin' and not (
|
||||
@@ -448,14 +450,18 @@ async def get_tools_by_id(id: str, user=Depends(get_verified_user), db: AsyncSes
|
||||
|
||||
@router.post('/id/{id}/update', response_model=ToolModel | None)
|
||||
async def update_tools_by_id(
|
||||
request: Request, id: str, form_data: ToolForm,
|
||||
user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session),
|
||||
request: Request,
|
||||
id: str,
|
||||
form_data: ToolForm,
|
||||
user=Depends(get_verified_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
"""Update an existing tool's source code and metadata."""
|
||||
tools = await Tools.get_tool_by_id(id, db=db)
|
||||
if not tools:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail=ERROR_MESSAGES.NOT_FOUND,
|
||||
)
|
||||
|
||||
# Is the user the original creator, in a group with write access, or an admin
|
||||
|
||||
@@ -384,7 +384,9 @@ async def get_user_info_by_session_user(user=Depends(get_verified_user), db: Asy
|
||||
|
||||
@router.post('/user/info/update', response_model=dict | None)
|
||||
async def update_user_info_by_session_user( # PATCH-style merge
|
||||
form_data: dict, user=Depends(get_verified_user), db: AsyncSession = Depends(get_async_session),
|
||||
form_data: dict,
|
||||
user=Depends(get_verified_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
"""Merge caller-supplied fields into the current user's info dict.
|
||||
|
||||
@@ -401,6 +403,7 @@ async def update_user_info_by_session_user( # PATCH-style merge
|
||||
)
|
||||
return updated.info
|
||||
|
||||
|
||||
############################
|
||||
# GetUserById
|
||||
############################
|
||||
@@ -538,7 +541,8 @@ async def get_user_active_status_by_id(
|
||||
|
||||
@router.post('/{user_id}/update', response_model=UserModel | None)
|
||||
async def update_user_by_id(
|
||||
user_id: str, form_data: UserUpdateForm,
|
||||
user_id: str,
|
||||
form_data: UserUpdateForm,
|
||||
session_user: UserModel = Depends(get_admin_user),
|
||||
db: AsyncSession = Depends(get_async_session),
|
||||
):
|
||||
@@ -743,27 +747,15 @@ async def get_user_preview(
|
||||
'user': {'id': target_user.id, 'name': target_user.name},
|
||||
'groups': [{'id': g.id, 'name': g.name} for g in user_groups],
|
||||
'models': {
|
||||
'items': [
|
||||
{'id': m.id, 'name': m.name}
|
||||
for m in active_models
|
||||
if m.id in accessible_model_ids
|
||||
],
|
||||
'items': [{'id': m.id, 'name': m.name} for m in active_models if m.id in accessible_model_ids],
|
||||
'total': len(active_models),
|
||||
},
|
||||
'knowledge': {
|
||||
'items': [
|
||||
{'id': k.id, 'name': k.name}
|
||||
for k in all_knowledge
|
||||
if k.id in accessible_knowledge_ids
|
||||
],
|
||||
'items': [{'id': k.id, 'name': k.name} for k in all_knowledge if k.id in accessible_knowledge_ids],
|
||||
'total': len(all_knowledge),
|
||||
},
|
||||
'tools': {
|
||||
'items': [
|
||||
{'id': t.id, 'name': t.name}
|
||||
for t in all_tools
|
||||
if t.id in accessible_tool_ids
|
||||
],
|
||||
'items': [{'id': t.id, 'name': t.name} for t in all_tools if t.id in accessible_tool_ids],
|
||||
'total': len(all_tools),
|
||||
},
|
||||
}
|
||||
Reference in new issue
Block a user