Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion ex_app/lib/all_tools/lib/task_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,16 @@
from niquests import ConnectionError, Timeout
from pydantic import BaseModel, ValidationError

from ex_app.lib.errors import UserFacingError
from ex_app.lib.logger import log


class Task(BaseModel):
id: int
status: str
output: dict[str, typing.Any] | None = None
# Serialized by Nextcloud 33+; the admin-facing `errorMessage` is never exposed over OCS
userFacingErrorMessage: str | None = None

class Response(BaseModel):
task: Task
Expand Down Expand Up @@ -77,7 +80,12 @@ async def run_task(nc: AsyncNextcloudApp, type, task_input):
except ValidationError as e:
raise Exception("Failed to parse Nextcloud TaskProcessing task result") from e
if task.status != "STATUS_SUCCESSFUL":
raise Exception("Nextcloud TaskProcessing Task failed")
if task.userFacingErrorMessage:
raise UserFacingError(
f"Nextcloud TaskProcessing task of type {type} failed: {task.userFacingErrorMessage}",
task.userFacingErrorMessage,
)
raise UserFacingError(f"Nextcloud TaskProcessing task of type {type} failed")

if not isinstance(task.output, dict) or all(x not in ACCEPTED_OUTPUT_KEYS for x in task.output):
raise Exception(f'Expected one of {ACCEPTED_OUTPUT_KEYS} in Nextcloud TaskProcessing task result')
Expand Down
16 changes: 16 additions & 0 deletions ex_app/lib/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later


class UserFacingError(Exception):
"""An error carrying a message that is safe to show to the end user.

Nextcloud TaskProcessing keeps two error strings per task: `errorMessage` for
admins/logs and `userFacingErrorMessage` for the user. Sub-tasks we schedule
(text2text chat, image generation, …) only expose the latter over the OCS API,
so we carry it along and report it on our own task as well.
"""

def __init__(self, message: str, user_facing_message: str | None = None):
super().__init__(message)
self.user_facing_message = user_facing_message
13 changes: 12 additions & 1 deletion ex_app/lib/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
SettingsFieldType)

from ex_app.lib.agent import react
from ex_app.lib.errors import UserFacingError
from ex_app.lib.logger import log
from ex_app.lib.mcp_server import UserAuthMiddleware, ToolListMiddleware
from ex_app.lib.provider import provider, multimodal_provider
Expand Down Expand Up @@ -181,6 +182,16 @@ async def background_thread_task():
NUM_RUNNING_TASKS_LOCK = asyncio.Lock()
NUM_RUNNING_TASKS = 0

async def report_error(nc: AsyncNextcloudApp, task_id: int, e: Exception):
"""Report a failed task, passing on the user-facing error message when we have one."""
# The user-facing message is only picked up by Nextcloud 33+, older versions ignore it.
await nc.providers.task_processing.report_result(
task_id,
error_message=str(e),
user_facing_error_message=e.user_facing_message if isinstance(e, UserFacingError) else None,
)


async def handle_task(task, nc: AsyncNextcloudApp):
global NUM_RUNNING_TASKS
try:
Expand Down Expand Up @@ -213,7 +224,7 @@ async def stream_output(intermediate_output):
try:
tb_str = ''.join(traceback.format_exception(e))
await log(nc, LogLvl.ERROR, "Error: " + tb_str)
await nc.providers.task_processing.report_result(task["id"], error_message=str(e))
await report_error(nc, task["id"], e)
except (NextcloudException, RequestException) as net_err:
tb_str = ''.join(traceback.format_exception(net_err))
await log(nc, LogLvl.WARNING, "Network error in reporting the error: " + tb_str)
Expand Down
27 changes: 23 additions & 4 deletions ex_app/lib/nc_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

from langchain_core.language_models.chat_models import BaseChatModel

from ex_app.lib.errors import UserFacingError
from ex_app.lib.logger import log


Expand Down Expand Up @@ -73,6 +74,18 @@ class Task(BaseModel):
status: str
output: dict[str, typing.Any] | None = None
preferStreaming: bool | None = None
# Serialized by Nextcloud 33+; the admin-facing `errorMessage` is never exposed over OCS
userFacingErrorMessage: str | None = None


def task_failed_error(task: Task) -> UserFacingError:
"""Turn a failed sub-task into an error that passes the provider's user-facing message on."""
if task.userFacingErrorMessage:
return UserFacingError(
f"Nextcloud TaskProcessing Task failed: {task.userFacingErrorMessage}",
task.userFacingErrorMessage,
)
return UserFacingError("Nextcloud TaskProcessing Task failed")
Comment thread
kyteinsky marked this conversation as resolved.


class Response(BaseModel):
Expand Down Expand Up @@ -334,10 +347,13 @@ async def _agenerate(
raise

if task.status == "STATUS_FAILED":
raise Exception("Nextcloud TaskProcessing Task failed")
raise task_failed_error(task)

if task.status in ("STATUS_RUNNING", "STATUS_SCHEDULED"):
raise Exception("Nextcloud TaskProcessing Task timed out")
raise UserFacingError(
"Nextcloud TaskProcessing Task timed out",
"The language model did not respond in time. Please try again.",
)

message = self._task_to_message(task)
return ChatResult(generations=[ChatGeneration(message=message)])
Expand Down Expand Up @@ -381,10 +397,13 @@ async def _astream(
streamed_output = current_output

if task.status == "STATUS_FAILED":
raise Exception("Nextcloud TaskProcessing Task failed")
raise task_failed_error(task)

if task.status in ("STATUS_RUNNING", "STATUS_SCHEDULED"):
raise Exception("Nextcloud TaskProcessing Task timed out")
raise UserFacingError(
"Nextcloud TaskProcessing Task timed out",
"The language model did not respond in time. Please try again.",
)

final_output = self._task_output_text(task)
if final_output is None:
Expand Down
Loading