From 865e77fe5f5fdbe60a116e9ea53da0dc73310c1f Mon Sep 17 00:00:00 2001 From: Jana Peper Date: Thu, 29 Jan 2026 10:44:59 +0100 Subject: [PATCH 1/6] feat: retry task scheduling for connection errors Signed-off-by: Jana Peper --- ex_app/lib/all_tools/lib/task_processing.py | 37 ++++++++++++++------ ex_app/lib/nc_model.py | 38 +++++++++++++++------ 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/ex_app/lib/all_tools/lib/task_processing.py b/ex_app/lib/all_tools/lib/task_processing.py index 770eef6..fc92f55 100644 --- a/ex_app/lib/all_tools/lib/task_processing.py +++ b/ex_app/lib/all_tools/lib/task_processing.py @@ -3,7 +3,7 @@ import time import typing -import httpx +from niquests import ConnectionError, ProxyError, SSLError, TimeoutConnectTimeout, ReadTimeout from nc_py_api import NextcloudException from nc_py_api.ex_app import LogLvl from pydantic import BaseModel, ValidationError @@ -20,11 +20,27 @@ class Response(BaseModel): task: Task def run_task(nc, type, task_input): - response = nc.ocs( - "POST", - "/ocs/v1.php/taskprocessing/schedule", - json={"type": type, "appId": "context_agent", "input": task_input}, - ) + i = 0 + while i < 20: + try: + response = nc.ocs( + "POST", + "/ocs/v1.php/taskprocessing/schedule", + json={"type": type, "appId": "context_agent", "input": task_input}, + ) + break + except ( + ConnectionError, + ProxyError, + SSLError, + TimeoutConnectTimeout, + ReadTimeout + + ) as e: + log(nc, LogLvl.DEBUG, "Ignored error during task scheduling") + i += 1 + sleep(1) + continue try: task = Response.model_validate(response).task @@ -38,10 +54,11 @@ def run_task(nc, type, task_input): try: response = nc.ocs("GET", f"/ocs/v1.php/taskprocessing/task/{task.id}") except ( - httpx.RemoteProtocolError, - httpx.ReadError, - httpx.LocalProtocolError, - httpx.PoolTimeout, + ConnectionError, + ProxyError, + SSLError, + TimeoutConnectTimeout, + ReadTimeout ) as e: log(nc, LogLvl.DEBUG, "Ignored error during task polling") time.sleep(5) diff --git a/ex_app/lib/nc_model.py b/ex_app/lib/nc_model.py index c917654..363c341 100644 --- a/ex_app/lib/nc_model.py +++ b/ex_app/lib/nc_model.py @@ -5,7 +5,7 @@ import typing from typing import Optional, Any, Sequence, Union, Callable -import httpx +from niquests import ConnectionError, ProxyError, SSLError, TimeoutConnectTimeout, ReadTimeout from langchain_core.callbacks import CallbackManagerForLLMRun from langchain_core.language_models import LanguageModelInput from langchain_core.messages import BaseMessage, AIMessage @@ -90,11 +90,27 @@ def _generate( log(nc, LogLvl.DEBUG, task_input) - response = nc.ocs( - "POST", - "/ocs/v1.php/taskprocessing/schedule", - json={"type": "core:text2text:chatwithtools", "appId": "context_agent", "input": task_input}, - ) + i = 0 + while i < 20: + try: + response = nc.ocs( + "POST", + "/ocs/v1.php/taskprocessing/schedule", + json={"type": "core:text2text:chatwithtools", "appId": "context_agent", "input": task_input}, + ) + break + except ( + ConnectionError, + ProxyError, + SSLError, + TimeoutConnectTimeout, + ReadTimeout + + ) as e: + log(nc, LogLvl.DEBUG, "Ignored error during task scheduling") + i += 1 + sleep(1) + continue try: task = Response.model_validate(response).task @@ -109,10 +125,12 @@ def _generate( try: response = nc.ocs("GET", f"/ocs/v1.php/taskprocessing/task/{task.id}") except ( - httpx.RemoteProtocolError, - httpx.ReadError, - httpx.LocalProtocolError, - httpx.PoolTimeout, + ConnectionError, + ProxyError, + SSLError, + TimeoutConnectTimeout, + ReadTimeout + ) as e: log(nc, LogLvl.DEBUG, "Ignored error during task polling") time.sleep(5) From 58cfd5d29e126e375f2dd2359257264d184273bb Mon Sep 17 00:00:00 2001 From: Jana Peper Date: Thu, 29 Jan 2026 10:45:18 +0100 Subject: [PATCH 2/6] chore: switch from httpx to niquests Signed-off-by: Jana Peper --- ex_app/lib/all_tools/calendar.py | 12 +++++++----- ex_app/lib/all_tools/deck.py | 1 - ex_app/lib/all_tools/here.py | 4 ++-- ex_app/lib/all_tools/mail.py | 11 ++++++----- ex_app/lib/all_tools/openstreetmap.py | 8 ++++---- ex_app/lib/all_tools/weather.py | 4 ++-- poetry.lock | 8 ++++---- pyproject.toml | 1 + 8 files changed, 26 insertions(+), 23 deletions(-) diff --git a/ex_app/lib/all_tools/calendar.py b/ex_app/lib/all_tools/calendar.py index 6ecb1ec..0725aac 100644 --- a/ex_app/lib/all_tools/calendar.py +++ b/ex_app/lib/all_tools/calendar.py @@ -4,7 +4,7 @@ from time import sleep from typing import Optional -import httpx +from niquests import ConnectionError, ProxyError, SSLError, TimeoutConnectTimeout, ReadTimeout import pytz from ics import Calendar, Event, Attendee, Organizer, Todo from langchain_core.tools import tool @@ -91,10 +91,12 @@ def schedule_event(calendar_name: str, title: str, description: str, start_date: json = nc.ocs('GET', '/ocs/v2.php/cloud/user') break except ( - httpx.RemoteProtocolError, - httpx.ReadError, - httpx.LocalProtocolError, - httpx.PoolTimeout, + ConnectionError, + ProxyError, + SSLError, + TimeoutConnectTimeout, + ReadTimeout + ) as e: log(nc, LogLvl.DEBUG, "Ignored error during task polling") i += 1 diff --git a/ex_app/lib/all_tools/deck.py b/ex_app/lib/all_tools/deck.py index 4c26b91..cba72bf 100644 --- a/ex_app/lib/all_tools/deck.py +++ b/ex_app/lib/all_tools/deck.py @@ -4,7 +4,6 @@ from time import sleep from typing import Optional -import httpx import pytz from langchain_core.tools import tool from nc_py_api import Nextcloud diff --git a/ex_app/lib/all_tools/here.py b/ex_app/lib/all_tools/here.py index 887ff1a..be44df8 100644 --- a/ex_app/lib/all_tools/here.py +++ b/ex_app/lib/all_tools/here.py @@ -4,7 +4,7 @@ import datetime import urllib.parse -import httpx +import niquests from langchain_core.tools import tool from nc_py_api import Nextcloud @@ -30,7 +30,7 @@ def get_public_transport_route_for_coordinates(origin_lat: str, origin_lon: str, if departure_time is None: departure_time = urllib.parse.quote_plus(datetime.datetime.now(datetime.UTC).isoformat()) api_key = nc.appconfig_ex.get_value('here_api') - res = httpx.get('https://transit.hereapi.com/v8/routes?transportMode=car&origin=' + res = niquests.get('https://transit.hereapi.com/v8/routes?transportMode=car&origin=' + origin_lat + ',' + origin_lon + '&destination=' + destination_lat + ',' + destination_lon + '&alternatives=' + str(routes-1) + '&departureTime=' + departure_time + '&apikey=' + api_key) json = res.json() diff --git a/ex_app/lib/all_tools/mail.py b/ex_app/lib/all_tools/mail.py index 38a99a2..cee3dda 100644 --- a/ex_app/lib/all_tools/mail.py +++ b/ex_app/lib/all_tools/mail.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later from time import sleep -import httpx +from niquests import ConnectionError, ProxyError, SSLError, TimeoutConnectTimeout, ReadTimeout from langchain_core.tools import tool from nc_py_api import Nextcloud from nc_py_api.ex_app import LogLvl @@ -35,10 +35,11 @@ def send_email(subject: str, body: str, account_id: int, from_email: str, to_ema 'to': [{'label': '', 'email': email} for email in to_emails], }) except ( - httpx.RemoteProtocolError, - httpx.ReadError, - httpx.LocalProtocolError, - httpx.PoolTimeout, + ConnectionError, + ProxyError, + SSLError, + TimeoutConnectTimeout, + ReadTimeout ) as e: log(nc, LogLvl.DEBUG, "Ignored error during task polling") i += 1 diff --git a/ex_app/lib/all_tools/openstreetmap.py b/ex_app/lib/all_tools/openstreetmap.py index 4c910a3..028ac24 100644 --- a/ex_app/lib/all_tools/openstreetmap.py +++ b/ex_app/lib/all_tools/openstreetmap.py @@ -6,7 +6,7 @@ import urllib.parse from time import sleep -import httpx +import niquests from langchain_core.tools import tool from nc_py_api import Nextcloud @@ -22,7 +22,7 @@ def get_coordinates_for_address(address: str) -> (str, str): :param address: the address to calculate the coordinates for :return: a tuple of latitude and longitude """ - res = httpx.get('https://nominatim.openstreetmap.org/search', params={'q': address, 'format': 'json', 'addressdetails': '1', 'extratags': '1', 'namedetails': '1', 'limit': '1'}) + res = niquests.get('https://nominatim.openstreetmap.org/search', params={'q': address, 'format': 'json', 'addressdetails': '1', 'extratags': '1', 'namedetails': '1', 'limit': '1'}) json = res.json() if 'error' in json: raise Exception(json['error']) @@ -56,7 +56,7 @@ def get_osm_route(profile: str, origin_lat: str, origin_lon: str, destination_la profile_num = "2" url = f'https://routing.openstreetmap.de/{profile}/route/v1/driving/{origin_lon},{origin_lat};{destination_lon},{destination_lat}?overview=false&steps=true' map_url = f' https://routing.openstreetmap.de/?loc={origin_lat}%2C{origin_lon}&loc={destination_lat}%2C{destination_lon}&srv={profile_num}' - res = httpx.get(url) + res = niquests.get(url) json = res.json() return {'route_json_description': json, 'map_url': map_url} @@ -70,7 +70,7 @@ def get_osm_link(location: str): :return: URL """ - res = httpx.get('https://nominatim.openstreetmap.org/search', params={'q': location, 'format': 'json','limit': '1'}) + res = niquests.get('https://nominatim.openstreetmap.org/search', params={'q': location, 'format': 'json','limit': '1'}) json = res.json() if 'error' in json: raise Exception(json['error']) diff --git a/ex_app/lib/all_tools/weather.py b/ex_app/lib/all_tools/weather.py index 7bc59fa..db420ae 100644 --- a/ex_app/lib/all_tools/weather.py +++ b/ex_app/lib/all_tools/weather.py @@ -3,7 +3,7 @@ import typing import urllib.parse -import httpx +import niquests from langchain_core.tools import tool from nc_py_api import Nextcloud @@ -20,7 +20,7 @@ def get_current_weather_for_coordinates(lat: str, lon: str) -> dict[str, typing. :param lon: Longitude :return: """ - res = httpx.get('https://api.met.no/weatherapi/locationforecast/2.0/compact', params={ + res = niquests.get('https://api.met.no/weatherapi/locationforecast/2.0/compact', params={ 'lat': lat, 'lon': lon, }, diff --git a/poetry.lock b/poetry.lock index 261baa1..0898c53 100644 --- a/poetry.lock +++ b/poetry.lock @@ -2110,14 +2110,14 @@ docs = ["autodoc-pydantic (>=2.0.1)", "caldav (==1.3.6)", "sphinx (<8)", "sphinx [[package]] name = "niquests" -version = "3.15.2" +version = "3.17.0" description = "Niquests is a simple, yet elegant, HTTP library. It is a drop-in replacement for Requests, which is under feature freeze." optional = false python-versions = ">=3.7" groups = ["main"] files = [ - {file = "niquests-3.15.2-py3-none-any.whl", hash = "sha256:2446e3602ba1418434822f5c1fcf8b8d1b52a3c296d2808a1ab7de4cf1312d99"}, - {file = "niquests-3.15.2.tar.gz", hash = "sha256:8076b1d2ff957022d52b2216ca7df92d92ce426d19a7ed63c7fd4fd630ab6c2b"}, + {file = "niquests-3.17.0-py3-none-any.whl", hash = "sha256:3930d94fce367385950dd545f913e7cfc6457feda76aecafeb324aae45da9fe1"}, + {file = "niquests-3.17.0.tar.gz", hash = "sha256:1f4f337a973215c76f6f6471504fedab9dc6187203284146081e1bd3d2a311fc"}, ] [package.dependencies] @@ -4257,4 +4257,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "dece81e5f1b7f2d561d0c3b2478eeb5e2032d1b6157c37b9d6aa11a0a3fc7ec3" +content-hash = "f5266cfc8797e210c471b6c96ba26325608a9e1ac1930dcbcdab8030d888f760" diff --git a/pyproject.toml b/pyproject.toml index dabc422..8e90793 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,6 +22,7 @@ duckduckgo-search = "^8.0.1" uvicorn = "^0.34.2" langchain-mcp-adapters = "^0.1.9" fastmcp = "^2.11.2" +niquests = "^3.17.0" [build-system] requires = ["poetry-core"] From bc6d53aa0e4dd0b11e7b7cc2521cc7793da942f2 Mon Sep 17 00:00:00 2001 From: Jana Peper Date: Thu, 29 Jan 2026 11:06:58 +0100 Subject: [PATCH 3/6] chore: fix error types Signed-off-by: Jana Peper --- ex_app/lib/all_tools/calendar.py | 8 ++------ ex_app/lib/all_tools/lib/task_processing.py | 12 +++--------- ex_app/lib/all_tools/mail.py | 7 ++----- ex_app/lib/nc_model.py | 12 +++--------- 4 files changed, 10 insertions(+), 29 deletions(-) diff --git a/ex_app/lib/all_tools/calendar.py b/ex_app/lib/all_tools/calendar.py index 0725aac..301997e 100644 --- a/ex_app/lib/all_tools/calendar.py +++ b/ex_app/lib/all_tools/calendar.py @@ -4,7 +4,7 @@ from time import sleep from typing import Optional -from niquests import ConnectionError, ProxyError, SSLError, TimeoutConnectTimeout, ReadTimeout +from niquests import ConnectionError, Timeout import pytz from ics import Calendar, Event, Attendee, Organizer, Todo from langchain_core.tools import tool @@ -92,11 +92,7 @@ def schedule_event(calendar_name: str, title: str, description: str, start_date: break except ( ConnectionError, - ProxyError, - SSLError, - TimeoutConnectTimeout, - ReadTimeout - + Timeout ) as e: log(nc, LogLvl.DEBUG, "Ignored error during task polling") i += 1 diff --git a/ex_app/lib/all_tools/lib/task_processing.py b/ex_app/lib/all_tools/lib/task_processing.py index fc92f55..59e226c 100644 --- a/ex_app/lib/all_tools/lib/task_processing.py +++ b/ex_app/lib/all_tools/lib/task_processing.py @@ -3,7 +3,7 @@ import time import typing -from niquests import ConnectionError, ProxyError, SSLError, TimeoutConnectTimeout, ReadTimeout +from niquests import ConnectionError, Timeout from nc_py_api import NextcloudException from nc_py_api.ex_app import LogLvl from pydantic import BaseModel, ValidationError @@ -31,10 +31,7 @@ def run_task(nc, type, task_input): break except ( ConnectionError, - ProxyError, - SSLError, - TimeoutConnectTimeout, - ReadTimeout + Timeout ) as e: log(nc, LogLvl.DEBUG, "Ignored error during task scheduling") @@ -55,10 +52,7 @@ def run_task(nc, type, task_input): response = nc.ocs("GET", f"/ocs/v1.php/taskprocessing/task/{task.id}") except ( ConnectionError, - ProxyError, - SSLError, - TimeoutConnectTimeout, - ReadTimeout + Timeout ) as e: log(nc, LogLvl.DEBUG, "Ignored error during task polling") time.sleep(5) diff --git a/ex_app/lib/all_tools/mail.py b/ex_app/lib/all_tools/mail.py index cee3dda..c37109e 100644 --- a/ex_app/lib/all_tools/mail.py +++ b/ex_app/lib/all_tools/mail.py @@ -2,7 +2,7 @@ # SPDX-License-Identifier: AGPL-3.0-or-later from time import sleep -from niquests import ConnectionError, ProxyError, SSLError, TimeoutConnectTimeout, ReadTimeout +from niquests import ConnectionError, Timeout from langchain_core.tools import tool from nc_py_api import Nextcloud from nc_py_api.ex_app import LogLvl @@ -36,10 +36,7 @@ def send_email(subject: str, body: str, account_id: int, from_email: str, to_ema }) except ( ConnectionError, - ProxyError, - SSLError, - TimeoutConnectTimeout, - ReadTimeout + Timeout ) as e: log(nc, LogLvl.DEBUG, "Ignored error during task polling") i += 1 diff --git a/ex_app/lib/nc_model.py b/ex_app/lib/nc_model.py index 363c341..cf2d6cb 100644 --- a/ex_app/lib/nc_model.py +++ b/ex_app/lib/nc_model.py @@ -5,7 +5,7 @@ import typing from typing import Optional, Any, Sequence, Union, Callable -from niquests import ConnectionError, ProxyError, SSLError, TimeoutConnectTimeout, ReadTimeout +from niquests import ConnectionError, Timeout from langchain_core.callbacks import CallbackManagerForLLMRun from langchain_core.language_models import LanguageModelInput from langchain_core.messages import BaseMessage, AIMessage @@ -101,10 +101,7 @@ def _generate( break except ( ConnectionError, - ProxyError, - SSLError, - TimeoutConnectTimeout, - ReadTimeout + Timeout ) as e: log(nc, LogLvl.DEBUG, "Ignored error during task scheduling") @@ -126,10 +123,7 @@ def _generate( response = nc.ocs("GET", f"/ocs/v1.php/taskprocessing/task/{task.id}") except ( ConnectionError, - ProxyError, - SSLError, - TimeoutConnectTimeout, - ReadTimeout + Timeout ) as e: log(nc, LogLvl.DEBUG, "Ignored error during task polling") From 8cb17c3b1c0b2c991ee19ea24680eb5ee747e827 Mon Sep 17 00:00:00 2001 From: Jana Peper Date: Thu, 29 Jan 2026 11:33:59 +0100 Subject: [PATCH 4/6] feat: bump max nc version to 34 Signed-off-by: Jana Peper --- appinfo/info.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index 97a93a7..9bb160a 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -39,7 +39,7 @@ Positive: https://github.com/nextcloud/context_agent/blob/main/img/screenshot.png?raw=true https://github.com/nextcloud/context_agent - + From 6faa943c2c368736aef94b41df58269ff064f621 Mon Sep 17 00:00:00 2001 From: Jana Peper Date: Thu, 29 Jan 2026 11:51:35 +0100 Subject: [PATCH 5/6] test: add stable33 to test matrix Signed-off-by: Jana Peper --- .github/workflows/integration_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index 6ec949a..eed04f5 100644 --- a/.github/workflows/integration_test.yml +++ b/.github/workflows/integration_test.yml @@ -28,7 +28,7 @@ jobs: matrix: php-versions: [ '8.2' ] databases: [ 'sqlite' ] - server-versions: [ 'master', 'stable32', 'stable31' ] + server-versions: [ 'master', 'stable33', 'stable32', 'stable31' ] name: Integration test on ☁️${{ matrix.server-versions }} 🐘${{ matrix.php-versions }} From fc5bc7887f421d0cd5dd15ce4a35d9e88792a8d7 Mon Sep 17 00:00:00 2001 From: Jana Peper Date: Thu, 29 Jan 2026 12:51:53 +0100 Subject: [PATCH 6/6] test: change used test tool to youtube Signed-off-by: Jana Peper --- .github/workflows/integration_test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/integration_test.yml b/.github/workflows/integration_test.yml index eed04f5..f93597e 100644 --- a/.github/workflows/integration_test.yml +++ b/.github/workflows/integration_test.yml @@ -198,7 +198,7 @@ jobs: CREDS: "alice:alice" run: | sleep 300 - TASK=$(curl -X POST -u "$CREDS" -H "oCS-APIRequest: true" -H "Content-type: application/json" http://localhost:8080/ocs/v2.php/taskprocessing/schedule?format=json --data-raw '{"input": {"input": "Search duckduckgo for Nextcloud", "confirmation":1, "conversation_token": ""},"type":"core:contextagent:interaction", "appId": "test", "customId": ""}') + TASK=$(curl -X POST -u "$CREDS" -H "oCS-APIRequest: true" -H "Content-type: application/json" http://localhost:8080/ocs/v2.php/taskprocessing/schedule?format=json --data-raw '{"input": {"input": "Search youtube for videos about Nextcloud", "confirmation":1, "conversation_token": ""},"type":"core:contextagent:interaction", "appId": "test", "customId": ""}') echo $TASK TASK_ID=$(echo $TASK | jq '.ocs.data.task.id') NEXT_WAIT_TIME=0 @@ -215,7 +215,7 @@ jobs: [ "$TASK_STATUS" == '"STATUS_SUCCESSFUL"' ] echo $TASK | jq '.ocs.data.task.output.output' echo $TASK | jq '.ocs.data.task.output.sources' - echo $TASK | jq '.ocs.data.task.output.sources' | grep -q 'duckduckgo_results_json' + echo $TASK | jq '.ocs.data.task.output.sources' | grep -q 'youtube_search' echo $TASK | jq '.ocs.data.task.output.output' | grep -q 'Nextcloud' - name: Show nextcloud logs