14 Commits

Author SHA1 Message Date
michael fd85758e3c fix: use httpx query parameters to correctly URL encode since timestamp (#6)
Co-authored-by: Michael <michael@example.com>
Reviewed-on: #6
2026-06-30 22:22:17 +02:00
michael 9fb56315c6 feat: make issue closing rules stricter in CoordinatorAgent system prompt (#5) 2026-06-30 22:07:48 +02:00
michael 6e2b57ed16 chore: remove accidental temp file '=2.0.0' (#4) 2026-06-30 22:05:42 +02:00
michael fd056e3ed0 feat: add merge_pull_request to GiteaClient (#3) 2026-06-30 22:01:44 +02:00
michael 9b72e20d5a feat: implement NotificationReaderAgent to pre-screen Gitea notifications (#2) 2026-06-30 22:01:07 +02:00
michael 287d20bc56 feat: implement Gitea notification polling and state persistence
Squash merged Gitea notification polling and state persistence.
2026-06-30 21:31:59 +02:00
michael edc8415571 refactor: clean up agent architecture and coordination loop
Squash merged refactoring of agent architecture.
2026-06-29 22:44:06 +02:00
michael 7f66d09d9e Feat/tool based coordinator routing (#5)
Co-authored-by: Michael <michael@example.com>
Reviewed-on: #5
2026-06-29 21:17:42 +02:00
michael e38a80532b Fix/planner comment thought process (#4)
Co-authored-by: Michael <michael@example.com>
Reviewed-on: #4
2026-06-29 20:49:06 +02:00
michael a0c247b152 feat: redesign issue processing to planning and question board with WIP PR workflow (#3)
Co-authored-by: Michael <michael@example.com>
Reviewed-on: #3
2026-06-29 20:35:46 +02:00
Michael bf13a979c2 feat: add output truncation and offset paging to all large-output tools
- coding_tools: list_files (max_entries=200, sorted), run_command
  (max_chars=8000, output_offset), grep_search (max_lines=100, offset);
  added _truncate_output helper for run_command paging
- pr_tools: get_pull_request_diff / get_pull_request_patch now truncate
  at 15k chars with hunk-boundary awareness and char_offset paging;
  get_pull_request_comments gains limit/offset paging
- issue_tools: get_issue_comments gains limit/offset paging
- file_tools: get_file_content / get_file_content_with_ref now paginate
  by line (offset=1, limit=250), matching existing read_file convention;
  added _paginate_lines helper
- research_tools: fetch_url gains char_offset parameter; _smart_truncate
  now slices from an offset and embeds next char_offset in the footer
- gitea_tools facade: all new params threaded through
2026-06-28 19:57:14 +02:00
michael c0c9278d75 feat: add web_search and fetch_url research tools (#2)
- Add ResearchTools class in gitea/tools/research_tools.py
  - web_search: DuckDuckGo HTML search, no API key required
  - fetch_url: httpx-based URL fetcher with HTML stripping + truncation
- Wire both tools into planning_tools and coding_tools_list in dispatcher.py
- Add 19 unit tests covering all methods, error paths, and edge cases

---------

Co-authored-by: Michael <michael@example.com>
Reviewed-on: #2
2026-06-28 19:01:55 +02:00
michael 0461c6c7ab Add .gitignore and pyproject.toml (#1)
### Findings and Changes

#### Changes:
- **Added **: Included a standard Python  to avoid tracking unnecessary files (e.g., , , ).
- **Added **: Prepared the project for better dependency management.
- **Enhanced Gitea Tools**:
    - Implemented  in .
    - Implemented  (via ) in .

#### Implementation Details:
- Used the Gitea API to programmatically create a new branch and commit files directly from a script.
- Verified that the NAME:
   tea - command line tool to interact with Gitea

USAGE:
   tea [global options] [command [command options]]

VERSION:
   Version: 0.14.1  golang: 1.26.3  go-sdk: v0.25.1

DESCRIPTION:
   tea is a productivity helper for Gitea. It can be used to manage most entities on
   one or multiple Gitea instances & provides local helpers like 'tea pr checkout'.

   tea tries to make use of context provided by the repository in $PWD if available.
   tea works best in a upstream/fork workflow, when the local main branch tracks the
   upstream repo. tea assumes that local git state is published on the remote before
   doing operations with tea.    Configuration is persisted in $XDG_CONFIG_HOME/tea.

COMMANDS:
   help, h  Shows a list of commands or help for one command

   ENTITIES:
     issues, issue, i                  List, create and update issues
     pulls, pull, pr                   Manage and checkout pull requests
     labels, label                     Manage issue labels
     milestones, milestone, ms         List and create milestones
     releases, release, r              Manage releases
     times, time, t                    Operate on tracked times of a repository's issues & pulls
     organizations, organization, org  List, create, delete organizations
     repos, repo                       Manage repositories
     branches, branch, b               Consult branches
     actions, action                   Manage repository actions
     webhooks, webhook, hooks, hook    Manage webhooks
     comment, c                        Add a comment to an issue / pr

   HELPERS:
     open, o                         Open something of the repository in web browser
     notifications, notification, n  Show notifications
     clone, C                        Clone a repository locally
     api                             Make an authenticated API request

   MISCELLANEOUS:
     whoami    Show current logged in user
     admin, a  Operations requiring admin access on the Gitea instance

   SETUP:
     logins, login      Log in to a Gitea server
     logout             Log out from a Gitea server
     ssh-keys, ssh-key  Manage SSH public keys

GLOBAL OPTIONS:
   --debug, --vvv  Enable debug mode
   --help, -h      show help
   --version, -v   print the version CLI can be used for automated PR creation.
- Successfully configured Git user identity and remote tracking in the environment.

---------

Co-authored-by: Michael <michael@example.com>
Reviewed-on: #1
2026-06-28 18:38:28 +02:00
michael ac6cff52dd Add README.md 2026-06-10 22:19:12 +02:00
5 changed files with 110 additions and 80 deletions
+55
View File
@@ -0,0 +1,55 @@
# Web Search Skill
## Purpose
Enable the agent to search the web using a self-hosted SearXNG instance for privacy-respecting searches.
## Configuration
- SearXNG Instance: `https://searxng.meeks.freeddns.org`
- API Endpoint: `/search`
- Supported Formats: `json`, `csv`, `rss`
## Usage
### Search the Web
When you need to look up information, find recent developments, or verify facts:
```
search_web(query="what is the latest version of Python", limit=10, language="en")
```
### Parameters
- `query` (required): The search query string
- `limit` (optional): Number of results to return (default: 10, max: 100)
- `language` (optional): Language code (e.g., "en", "sv", "de")
- `time_range` (optional): Time filter - "day", "month", or "year"
- `categories` (optional): Search category - "general", "images", "news", "videos", "science", "it", "music", "files", "map", "realtime"
### Example Queries
- `search_web(query="uv python package manager tutorial")`
- `search_web(query="searxng API documentation", time_range="month")`
- `search_web(query="gitea vs github", categories="general")`
## Implementation Details
The skill uses the SearXNG JSON API:
```
GET https://searxng.meeks.freeddns.org/search?q={query}&format=json&limit={limit}&language={language}
```
### Error Handling
- If the SearXNG instance is unreachable, log the error and try alternative search methods
- If the response format is invalid, parse what's available and report the issue
- If rate limited, wait and retry with exponential backoff
## When to Use
- Researching technical issues or solutions
- Looking up recent software updates or versions
- Finding documentation or tutorials
- Verifying facts or current information
- Investigating error messages or stack traces
- Finding similar projects or libraries
## When NOT to Use
- When you already have the information locally
- For simple factual questions that don't require current data
- When the user explicitly asks not to search the web
-21
View File
@@ -1,21 +0,0 @@
MIT License
Copyright (c) 2026 mi222eh
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
-11
View File
@@ -13,9 +13,6 @@ class AgentSettings(BaseSettings):
gitea_repos_root: str = ""
agent_model_id: str = "qwen/qwen3.6-35b-a3b"
agent_max_retries: int = 2
searxng_url: str = ""
searxng_username: str = ""
searxng_password: str = ""
def get_settings() -> AgentSettings:
@@ -31,15 +28,7 @@ GITEA_TOKEN: str = _agent_settings.gitea_token
GITEA_REPOS_ROOT: str = _agent_settings.gitea_repos_root
AGENT_MODEL_ID: str = _agent_settings.agent_model_id
AGENT_MAX_RETRIES: int = _agent_settings.agent_max_retries
SEARXNG_URL: str = _agent_settings.searxng_url
SEARXNG_USERNAME: str = _agent_settings.searxng_username
SEARXNG_PASSWORD: str = _agent_settings.searxng_password
import os
os.environ["GITEA_SERVER_URL"] = GITEA_URL
os.environ["GITEA_SERVER_TOKEN"] = GITEA_TOKEN
os.environ["SEARXNG_URL"] = SEARXNG_URL
os.environ["SEARXNG_USERNAME"] = SEARXNG_USERNAME
os.environ["SEARXNG_PASSWORD"] = SEARXNG_PASSWORD
+21 -21
View File
@@ -20,13 +20,6 @@ import time
from typing import Any
import httpx
from duckduckgo_search import DDGS # type: ignore[import-untyped]
from duckduckgo_search.exceptions import ( # type: ignore[import-untyped]
DuckDuckGoSearchException,
RatelimitException,
)
from gitea.config import SEARXNG_URL, SEARXNG_USERNAME, SEARXNG_PASSWORD
logger: logging.Logger = logging.getLogger("research-tools")
@@ -36,12 +29,10 @@ _USER_AGENT: str = (
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36"
)
# SearXNG instance
_SEARXNG_URL: str = SEARXNG_URL
_SEARXNG_USERNAME: str = SEARXNG_USERNAME
_SEARXNG_PASSWORD: str = SEARXNG_PASSWORD
# SearXNG instance — override with SEARXNG_URL env var
_SEARXNG_URL: str = os.getenv(
"SEARXNG_URL", "https://searxng.meeks.freeddns.org"
)
class ResearchTools:
@@ -187,9 +178,6 @@ class ResearchTools:
Returns formatted results string on success, or None if the instance
is unreachable so the caller can fall through to the next backend.
"""
if not _SEARXNG_URL:
logger.info("SearXNG URL is not configured; skipping SearXNG search.")
return None
params: dict[str, Any] = {
"q": query,
"format": "json",
@@ -199,13 +187,9 @@ class ResearchTools:
if time_range:
params["time_range"] = time_range
auth = None
if _SEARXNG_USERNAME and _SEARXNG_PASSWORD:
auth = (_SEARXNG_USERNAME, _SEARXNG_PASSWORD)
try:
with httpx.Client(
timeout=_DEFAULT_TIMEOUT, follow_redirects=True, auth=auth
timeout=_DEFAULT_TIMEOUT, follow_redirects=True
) as client:
response = client.get(
f"{_SEARXNG_URL}/search",
@@ -323,6 +307,22 @@ class ResearchTools:
logger.info("SearXNG unavailable; falling back to DuckDuckGo")
# --- Tier 2: DDGS library (handles sessions, cookies, rate limits) ---
try:
from duckduckgo_search import DDGS # type: ignore[import-untyped]
except ImportError:
logger.debug("duckduckgo-search not installed; using httpx fallback")
return self._web_search_fallback(query, num_results)
# Import exception types (package versions differ on exact names)
try:
from duckduckgo_search.exceptions import ( # type: ignore[import-untyped]
RatelimitException,
DuckDuckGoSearchException,
)
except ImportError:
RatelimitException = Exception # type: ignore[assignment,misc]
DuckDuckGoSearchException = Exception # type: ignore[assignment,misc]
last_exc: Exception | None = None
for attempt in range(3):
try:
+34 -27
View File
@@ -191,21 +191,6 @@ class TestSearchSearxng:
params = call_kwargs[1].get("params", call_kwargs[0][1] if len(call_kwargs[0]) > 1 else {})
assert params.get("time_range") == "month"
@patch("gitea.tools.research_tools.httpx.Client")
def test_passes_basic_auth_if_configured(
self, mock_cls: MagicMock, tools: ResearchTools
) -> None:
mock_cls.return_value.__enter__.return_value.get.return_value = (
self._make_searxng_response([])
)
with patch("gitea.tools.research_tools._SEARXNG_USERNAME", "user"), \
patch("gitea.tools.research_tools._SEARXNG_PASSWORD", "pass"):
tools._search_searxng("q", num_results=5)
mock_cls.assert_called_once()
kwargs = mock_cls.call_args[1]
assert kwargs.get("auth") == ("user", "pass")
@patch("gitea.tools.research_tools.httpx.Client")
def test_web_search_uses_searxng_first(
self, mock_cls: MagicMock, tools: ResearchTools
@@ -243,8 +228,7 @@ class TestWebSearch:
self, _mock_sleep: MagicMock, tools: ResearchTools
) -> None:
mock_cls = MagicMock(return_value=self._mock_ddgs(self._make_ddgs_result()))
with patch("gitea.tools.research_tools.ResearchTools._search_searxng", return_value=None), \
patch("gitea.tools.research_tools.DDGS", mock_cls):
with patch.dict("sys.modules", {"duckduckgo_search": MagicMock(DDGS=mock_cls)}):
result = tools.web_search("python httpx")
assert "[1]" in result
assert "foo.com" in result
@@ -254,16 +238,14 @@ class TestWebSearch:
self, _mock_sleep: MagicMock, tools: ResearchTools
) -> None:
mock_cls = MagicMock(return_value=self._mock_ddgs([]))
with patch("gitea.tools.research_tools.ResearchTools._search_searxng", return_value=None), \
patch("gitea.tools.research_tools.DDGS", mock_cls):
with patch.dict("sys.modules", {"duckduckgo_search": MagicMock(DDGS=mock_cls)}):
result = tools.web_search("xyzzy-not-real")
assert "No results" in result
def test_clamps_num_results_max(self, tools: ResearchTools) -> None:
ddgs_mock = self._mock_ddgs(self._make_ddgs_result())
mock_cls = MagicMock(return_value=ddgs_mock)
with patch("gitea.tools.research_tools.ResearchTools._search_searxng", return_value=None), \
patch("gitea.tools.research_tools.DDGS", mock_cls):
with patch.dict("sys.modules", {"duckduckgo_search": MagicMock(DDGS=mock_cls)}):
tools.web_search("q", num_results=999)
# DDGS.text should be called with max_results clamped to 20
ddgs_mock.text.assert_called_once()
@@ -273,8 +255,7 @@ class TestWebSearch:
def test_clamps_num_results_min(self, tools: ResearchTools) -> None:
ddgs_mock = self._mock_ddgs(self._make_ddgs_result())
mock_cls = MagicMock(return_value=ddgs_mock)
with patch("gitea.tools.research_tools.ResearchTools._search_searxng", return_value=None), \
patch("gitea.tools.research_tools.DDGS", mock_cls):
with patch.dict("sys.modules", {"duckduckgo_search": MagicMock(DDGS=mock_cls)}):
tools.web_search("q", num_results=0)
_, kwargs = ddgs_mock.text.call_args
assert kwargs.get("max_results", 0) >= 1
@@ -290,14 +271,40 @@ class TestWebSearch:
ddgs_mock.__exit__ = MagicMock(return_value=False)
ddgs_mock.text.side_effect = rate_exc
with patch("gitea.tools.research_tools.ResearchTools._search_searxng", return_value=None), \
patch("gitea.tools.research_tools.DDGS", return_value=ddgs_mock), \
patch("gitea.tools.research_tools.RatelimitException", type(rate_exc)), \
patch("gitea.tools.research_tools.DuckDuckGoSearchException", ValueError):
mock_module = MagicMock()
mock_module.DDGS = MagicMock(return_value=ddgs_mock)
# Make RatelimitException match our rate_exc type
mock_module.exceptions.RatelimitException = type(rate_exc)
mock_module.exceptions.DuckDuckGoSearchException = ValueError
with patch.dict("sys.modules", {
"duckduckgo_search": mock_module,
"duckduckgo_search.exceptions": mock_module.exceptions,
}):
result = tools.web_search("q")
assert isinstance(result, str) # Returns error string, not raise
def test_fallback_when_ddgs_not_installed(self, tools: ResearchTools) -> None:
"""When duckduckgo-search is not installed, uses httpx HTML fallback."""
import builtins
real_import = builtins.__import__
def mock_import(name: str, *args, **kwargs): # type: ignore[no-untyped-def]
if name == "duckduckgo_search":
raise ImportError("mocked missing")
return real_import(name, *args, **kwargs)
mock_response = MagicMock()
mock_response.text = ""
mock_response.raise_for_status = MagicMock()
with patch("builtins.__import__", side_effect=mock_import):
with patch("gitea.tools.research_tools.httpx.Client") as mock_cls:
mock_cls.return_value.__enter__.return_value.get.return_value = mock_response
result = tools.web_search("test")
assert isinstance(result, str)
# ------------------------------------------------------------------ #