13 Commits
Author SHA1 Message Date
armistace afc2f36e13 Merge pull request 'speed up CI dependency installation' (#29) from ci-speed-optimizations into master
Create Blog Article if new notes exist / prepare_blog_drafts_and_push (push) Successful in 7m13s
Reviewed-on: #29
2026-07-28 11:08:26 +10:00
armistace 1d18a37fa7 Merge branch 'master' into ci-speed-optimizations 2026-07-28 11:08:19 +10:00
armistace a1a534a86c Merge pull request 'bump all timeouts to 60 minutes for large context models' (#28) from bump-timeouts-60min into master
Create Blog Article if new notes exist / prepare_blog_drafts_and_push (push) Has been cancelled
Reviewed-on: #28
2026-07-28 11:07:43 +10:00
Hermes Agent c86d6e4e4d speed up CI dependency installation 2026-07-28 10:44:00 +10:00
Hermes Agent 5e014f50a5 bump all timeouts to 60 minutes for large context models 2026-07-28 10:38:09 +10:00
Hermes Agent d488c2be1d ci: retry after model swap 2026-07-28 10:32:43 +10:00
armistace 6d2b043eca Merge pull request 'udpate chromadb import to be lazy' (#27) from fix-random-illegal-instruction into master
Create Blog Article if new notes exist / prepare_blog_drafts_and_push (push) Failing after 28m29s
Reviewed-on: #27
2026-05-21 22:44:37 +10:00
armistace fa611192c8 reviewer fix 2026-05-21 22:44:04 +10:00
armistace e69b83694c udpate chromadb import to be lazy 2026-05-21 21:28:03 +10:00
armistace 1781a1dbf5 Merge pull request 'update repo manager to work of master not main' (#22) from git-creation-issue into master
Create Blog Article if new notes exist / prepare_blog_drafts_and_push (push) Failing after 10m5s
Reviewed-on: #22
2026-04-29 21:58:43 +10:00
armistace 64acdb29b2 update repo manager to work of master not main 2026-04-29 21:58:16 +10:00
armistace d148b95534 Merge pull request 'udpate to use bearer token correctly' (#21) from CREWAI-integration into master
Create Blog Article if new notes exist / prepare_blog_drafts_and_push (push) Failing after 19m39s
Reviewed-on: #21
2026-04-29 09:40:08 +10:00
armistace 9171248ae6 udpate to use bearer token correctly 2026-04-29 09:38:29 +10:00
6 changed files with 94 additions and 42 deletions
+5 -5
View File
@@ -15,12 +15,12 @@ jobs:
- name: Install dependencies
shell: bash
run: |
apt update && apt upgrade -y
apt install rustc cargo python-is-python3 pip python3-venv python3-virtualenv libmagic-dev git -y
virtualenv .venv
apt update -qq
apt install -y -qq python-is-python3 pip python3-venv libmagic-dev git
python -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
pip install -r requirements.txt
pip install --upgrade pip -q
pip install -r requirements.txt --prefer-binary -q
git config --global user.name "Blog Creator"
git config --global user.email "ridgway.infrastructure@gmail.com"
git config --global push.autoSetupRemote true
+1
View File
@@ -1,4 +1,5 @@
ollama
httpx
trilium-py
gitpython
PyGithub
+23 -4
View File
@@ -17,14 +17,19 @@ journalist draft is chunked, embedded, and stored in a collection; the editor
receives the top-N most relevant chunks as context.
"""
from __future__ import annotations
import json
import os
import random
import re
import string
from datetime import datetime
from typing import TYPE_CHECKING
if TYPE_CHECKING:
import chromadb # noqa: F811
import chromadb
from crewai.flow.flow import Flow, listen, start
from ollama import Client
from pydantic import BaseModel, ConfigDict
@@ -85,7 +90,21 @@ class BlogFlow(Flow[BlogFlowState]):
)
@staticmethod
def _get_chroma_client() -> chromadb.HttpClient:
def _get_chroma_client() -> "chromadb.HttpClient":
# Lazily import chromadb here rather than at module level.
# chromadb unconditionally loads hnswlib (a native C++ library
# compiled with AVX instructions) even when using HttpClient
# to talk to an external ChromaDB server. Deferring the import
# avoids "Illegal instruction" (SIGILL) crashes in environments
# that lack AVX support (e.g. act, older CI runners).
try:
import chromadb
except ImportError as exc:
raise RuntimeError(
"chromadb is required for the editor phase but could not be "
f"imported: {exc}"
) from exc
chroma_port = int(os.environ["CHROMA_PORT"])
return chromadb.HttpClient(host=os.environ["CHROMA_HOST"], port=chroma_port)
@@ -127,7 +146,7 @@ class BlogFlow(Flow[BlogFlowState]):
print(f"Error generating embeddings: {exc}")
return []
def _load_drafts_to_vector_db(self, drafts: list[str]) -> chromadb.Collection:
def _load_drafts_to_vector_db(self, drafts: list[str]) -> "chromadb.Collection":
"""Load journalist drafts into a new ChromaDB collection and return it."""
chroma = self._get_chroma_client()
collection_name = (
@@ -165,7 +184,7 @@ class BlogFlow(Flow[BlogFlowState]):
return collection
@staticmethod
def _query_vector_db(collection: chromadb.Collection, query_text: str) -> str:
def _query_vector_db(collection: "chromadb.Collection", query_text: str) -> str:
"""Query the ChromaDB collection and return the most relevant
document chunks joined as a single string."""
ollama_client = BlogFlow._get_ollama_client()
+2 -2
View File
@@ -140,8 +140,8 @@ class OllamaGenerator:
)
return response["message"]["content"]
# Retry mechanism with 30-minute timeout (same as the original)
timeout_seconds = 30 * 60
# Retry mechanism with 60-minute timeout (bumped for large context models)
timeout_seconds = 60 * 60
max_retries = 3
for attempt in range(max_retries):
@@ -4,17 +4,25 @@ Custom CrewAI tool that wraps Ollama's native web search API.
This tool allows CrewAI agents to perform web searches using an Ollama
subscription instead of third-party services like Serper or EXA.
Uses direct HTTP requests via httpx with explicit Authorization: Bearer
header to ensure the OLLAMA_API_KEY is properly passed to the Ollama cloud
API endpoint (https://ollama.com/api/web_search).
Requires:
- Ollama Python library: pip install ollama
- httpx library: pip install httpx (already a transitive dependency of crewai)
- OLLAMA_API_KEY environment variable set with your Ollama API key
Reference: https://docs.ollama.com/capabilities/web-search
"""
import os
import ollama
import httpx
from crewai.tools import BaseTool
from pydantic import BaseModel, Field
OLLAMA_WEB_SEARCH_URL = "https://ollama.com/api/web_search"
class OllamaWebSearchInput(BaseModel):
"""Input schema for OllamaWebSearchTool."""
@@ -42,6 +50,10 @@ class OllamaWebSearchTool(BaseTool):
The tool requires an Ollama subscription and the OLLAMA_API_KEY environment
variable to be set.
Authentication is handled by sending the OLLAMA_API_KEY as a Bearer token
in the Authorization header, as documented at:
https://docs.ollama.com/capabilities/web-search
Example usage:
from ai_generators.tools.ollama_web_search_tool import OllamaWebSearchTool
@@ -65,6 +77,9 @@ class OllamaWebSearchTool(BaseTool):
"""
Execute a web search and return formatted results.
Makes a POST request to https://ollama.com/api/web_search with the
OLLAMA_API_KEY as a Bearer token in the Authorization header.
Args:
query: The search query string
max_results: Maximum number of results to return (1-10)
@@ -72,16 +87,40 @@ class OllamaWebSearchTool(BaseTool):
Returns:
Formatted string with search results, each containing title, URL, and content
"""
api_key = os.environ.get("OLLAMA_API_KEY")
if not api_key:
return (
"Error: OLLAMA_API_KEY environment variable is not set. "
"Please set your Ollama API key."
)
try:
# Ensure API key is set
if not os.environ.get("OLLAMA_API_KEY"):
return "Error: OLLAMA_API_KEY environment variable is not set. Please set your Ollama API key."
response = httpx.post(
OLLAMA_WEB_SEARCH_URL,
json={"query": query, "max_results": max_results},
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
timeout=60.0,
)
# Perform the web search
response = ollama.web_search(query=query, max_results=max_results)
# Raise for HTTP errors so we can catch them with specific messages
if response.status_code == 401:
return (
"Authentication error: OLLAMA_API_KEY was rejected. "
"Your key may be invalid or expired. Please verify it at "
"https://ollama.com/settings/keys"
)
if response.status_code == 429:
return (
"Rate limit exceeded: Too many search requests. "
"Please wait a moment and try again."
)
response.raise_for_status()
# Extract and format results
results = response.get("results", [])
data = response.json()
results = data.get("results", [])
if not results:
return f"No search results found for query: '{query}'"
@@ -98,27 +137,20 @@ class OllamaWebSearchTool(BaseTool):
return "\n".join(formatted_results)
except Exception as exc:
return f"Error performing web search: {exc}"
def _handle_exception(self, exc: Exception) -> str:
"""Handle exceptions gracefully and return a user-friendly error message."""
error_message = str(exc)
# Check for common error types
if "authentication" in error_message.lower() or "401" in error_message:
except httpx.TimeoutException:
return (
"Authentication error: Your OLLAMA_API_KEY may be invalid or expired. "
"Please check your API key and ensure it's set correctly in the environment."
"Timeout error: The web search request timed out. "
"Please try again with a simpler query."
)
elif "rate limit" in error_message.lower() or "429" in error_message:
return "Rate limit exceeded: Too many search requests. Please wait a moment and try again."
elif (
"network" in error_message.lower() or "connection" in error_message.lower()
):
except httpx.ConnectError:
return (
"Network error: Unable to connect to Ollama's web search service. "
"Please check your internet connection and try again."
)
else:
return f"Search failed: {error_message}"
except httpx.HTTPStatusError as exc:
return (
f"HTTP error {exc.response.status_code} from Ollama web search API: "
f"{exc.response.text}"
)
except Exception as exc:
return f"Error performing web search: {exc}"
+4 -4
View File
@@ -42,7 +42,7 @@ class GitRepository:
print(f"Cloning failed: {e}")
return False
def fetch(self, remote_name="origin", ref_name="main"):
def fetch(self, remote_name="origin", ref_name="master"):
"""Fetch updates from a remote repository with authentication"""
try:
self.repo.remotes[remote_name].fetch(ref_name=ref_name)
@@ -51,7 +51,7 @@ class GitRepository:
print(f"Fetching failed: {e}")
return False
def pull(self, remote_name="origin", ref_name="main"):
def pull(self, remote_name="origin", ref_name="master"):
"""Pull updates from a remote repository with authentication"""
print("Pulling Latest Updates (if any)")
try:
@@ -93,8 +93,8 @@ class GitRepository:
self.repo.git.checkout(title)
self.pull(ref_name=title)
else:
# New branch, create from main
self.repo.git.checkout("-b", title, "origin/main")
# New branch, create from master
self.repo.git.checkout("-b", title, "origin/master")
# Ensure destination directory exists
dest_dir = f"{self.repo_path}src/content/"