Author SHA1 Message Date
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
4 changed files with 87 additions and 35 deletions
+1
View File
@@ -1,4 +1,5 @@
ollama ollama
httpx
trilium-py trilium-py
gitpython gitpython
PyGithub 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. receives the top-N most relevant chunks as context.
""" """
from __future__ import annotations
import json import json
import os import os
import random import random
import re import re
import string import string
from datetime import datetime 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 crewai.flow.flow import Flow, listen, start
from ollama import Client from ollama import Client
from pydantic import BaseModel, ConfigDict from pydantic import BaseModel, ConfigDict
@@ -85,7 +90,21 @@ class BlogFlow(Flow[BlogFlowState]):
) )
@staticmethod @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"]) chroma_port = int(os.environ["CHROMA_PORT"])
return chromadb.HttpClient(host=os.environ["CHROMA_HOST"], port=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}") print(f"Error generating embeddings: {exc}")
return [] 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.""" """Load journalist drafts into a new ChromaDB collection and return it."""
chroma = self._get_chroma_client() chroma = self._get_chroma_client()
collection_name = ( collection_name = (
@@ -165,7 +184,7 @@ class BlogFlow(Flow[BlogFlowState]):
return collection return collection
@staticmethod @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 """Query the ChromaDB collection and return the most relevant
document chunks joined as a single string.""" document chunks joined as a single string."""
ollama_client = BlogFlow._get_ollama_client() ollama_client = BlogFlow._get_ollama_client()
@@ -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 This tool allows CrewAI agents to perform web searches using an Ollama
subscription instead of third-party services like Serper or EXA. 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: 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 - OLLAMA_API_KEY environment variable set with your Ollama API key
Reference: https://docs.ollama.com/capabilities/web-search
""" """
import os import os
import ollama import httpx
from crewai.tools import BaseTool from crewai.tools import BaseTool
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
OLLAMA_WEB_SEARCH_URL = "https://ollama.com/api/web_search"
class OllamaWebSearchInput(BaseModel): class OllamaWebSearchInput(BaseModel):
"""Input schema for OllamaWebSearchTool.""" """Input schema for OllamaWebSearchTool."""
@@ -42,6 +50,10 @@ class OllamaWebSearchTool(BaseTool):
The tool requires an Ollama subscription and the OLLAMA_API_KEY environment The tool requires an Ollama subscription and the OLLAMA_API_KEY environment
variable to be set. 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: Example usage:
from ai_generators.tools.ollama_web_search_tool import OllamaWebSearchTool 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. 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: Args:
query: The search query string query: The search query string
max_results: Maximum number of results to return (1-10) max_results: Maximum number of results to return (1-10)
@@ -72,16 +87,40 @@ class OllamaWebSearchTool(BaseTool):
Returns: Returns:
Formatted string with search results, each containing title, URL, and content 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: try:
# Ensure API key is set response = httpx.post(
if not os.environ.get("OLLAMA_API_KEY"): OLLAMA_WEB_SEARCH_URL,
return "Error: OLLAMA_API_KEY environment variable is not set. Please set your Ollama API key." json={"query": query, "max_results": max_results},
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
},
timeout=30.0,
)
# Perform the web search # Raise for HTTP errors so we can catch them with specific messages
response = ollama.web_search(query=query, max_results=max_results) 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 data = response.json()
results = response.get("results", []) results = data.get("results", [])
if not results: if not results:
return f"No search results found for query: '{query}'" return f"No search results found for query: '{query}'"
@@ -98,27 +137,20 @@ class OllamaWebSearchTool(BaseTool):
return "\n".join(formatted_results) return "\n".join(formatted_results)
except Exception as exc: except httpx.TimeoutException:
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:
return ( return (
"Authentication error: Your OLLAMA_API_KEY may be invalid or expired. " "Timeout error: The web search request timed out. "
"Please check your API key and ensure it's set correctly in the environment." "Please try again with a simpler query."
) )
elif "rate limit" in error_message.lower() or "429" in error_message: except httpx.ConnectError:
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()
):
return ( return (
"Network error: Unable to connect to Ollama's web search service. " "Network error: Unable to connect to Ollama's web search service. "
"Please check your internet connection and try again." "Please check your internet connection and try again."
) )
else: except httpx.HTTPStatusError as exc:
return f"Search failed: {error_message}" 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}") print(f"Cloning failed: {e}")
return False 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""" """Fetch updates from a remote repository with authentication"""
try: try:
self.repo.remotes[remote_name].fetch(ref_name=ref_name) self.repo.remotes[remote_name].fetch(ref_name=ref_name)
@@ -51,7 +51,7 @@ class GitRepository:
print(f"Fetching failed: {e}") print(f"Fetching failed: {e}")
return False 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""" """Pull updates from a remote repository with authentication"""
print("Pulling Latest Updates (if any)") print("Pulling Latest Updates (if any)")
try: try:
@@ -93,8 +93,8 @@ class GitRepository:
self.repo.git.checkout(title) self.repo.git.checkout(title)
self.pull(ref_name=title) self.pull(ref_name=title)
else: else:
# New branch, create from main # New branch, create from master
self.repo.git.checkout("-b", title, "origin/main") self.repo.git.checkout("-b", title, "origin/master")
# Ensure destination directory exists # Ensure destination directory exists
dest_dir = f"{self.repo_path}src/content/" dest_dir = f"{self.repo_path}src/content/"