Browse documentation
CrewAI
A research tool for a research crew.
A tool
CrewAI tools are classes with a Pydantic argument schema.
import os, httpx
from typing import Type
from crewai.tools import BaseTool
from pydantic import BaseModel, Field
class SearchInput(BaseModel):
query: str = Field(description='Supports AND, OR, -exclude and "exact phrase".')
vertical: str | None = Field(None, description="e.g. code, science. Omit to auto-route.")
limit: int = Field(5, description="Maximum results, 1-20.")
class UnlobSearch(BaseTool):
name: str = "unlob_search"
description: str = (
"Search the web for passages. Returns passage text, not links, so the result "
"is usually the answer rather than something to fetch. Each hit carries "
"independent_sources: how many distinct sites assert it."
)
args_schema: Type[BaseModel] = SearchInput
def _run(self, query: str, vertical: str | None = None, limit: int = 5) -> str:
params = {
"q": query,
"limit": min(limit, 20),
# Applied here rather than left to the agent: drop single-source claims,
# and fold near-duplicates so the crew does not read one article five times.
"min_independent_sources": 2,
"collapse": "story",
}
if vertical:
params["vertical"] = vertical
r = httpx.get(
"https://api.unlob.com/search",
params=params,
headers={"x-api-key": os.environ["UNLOB_API_KEY"]},
timeout=20,
)
if r.status_code == 429 and "retry-after" not in r.headers:
return "Monthly quota exhausted. Do not retry; report this to the user."
r.raise_for_status()
body = r.json()
header = ("WARNING: incomplete results — part of the corpus was unreachable.\n\n"
if body.get("partial") else "")
hits = "\n\n".join(
f"{h['title']} — {h['url']} ({h.get('independent_sources', 0)} independent sources)\n{h['snippet']}"
for h in body["results"]
)
return header + (hits or "No results found.")
A verification tool worth pairing with it
The reason to use unlob in a crew rather than a generic search tool:
class CorroborateInput(BaseModel):
id: str = Field(description="A passage id from a search result.")
class UnlobCorroborate(BaseTool):
name: str = "unlob_corroborate"
description: str = (
"Check whether a claim is independently reported. Given a passage id, returns "
"the distinct hosts carrying that story, grouped and ranked by authority. Use "
"this before stating anything as established fact — forty results echoing one "
"source and four independent reports look identical in a result list."
)
args_schema: Type[BaseModel] = CorroborateInput
def _run(self, id: str) -> str:
r = httpx.get(
"https://api.unlob.com/corroborate",
params={"id": id},
headers={"x-api-key": os.environ["UNLOB_API_KEY"]},
timeout=20,
)
r.raise_for_status()
b = r.json()
hosts = ", ".join(s["host"] for s in b["sources"])
return (f"{b['independent_sources']} independent sources "
f"({b['merged_duplicates']} duplicates folded in): {hosts}")
Using them
from crewai import Agent, Task, Crew
researcher = Agent(
role="Research analyst",
goal="Find what is actually established about {topic}, and what is only asserted",
backstory=(
"You distinguish corroborated fact from repetition. You never state something "
"as established without checking how many independent sources carry it."
),
tools=[UnlobSearch(), UnlobCorroborate()],
)
task = Task(
description="Research {topic}. Corroborate every claim you intend to report.",
expected_output="A brief listing each finding with its independent source count.",
agent=researcher,
)
Crew(agents=[researcher], tasks=[task]).kickoff(inputs={"topic": "…"})
The backstory is doing real work there. A crew given a search tool will search; a crew told what distinguishes a fact from a repetition will use the second tool.