Skip to content

Commit 012bbe5

Browse files
committed
first commit
0 parents  commit 012bbe5

File tree

11 files changed

+444
-0
lines changed

11 files changed

+444
-0
lines changed

‎.dockerignore‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.env
2+
.mypy_cache/
3+
__pycache__/
4+
tests/
5+
llama-4-researcher.code-workspace

‎.gitignore‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
.env
2+
.mypy_cache/
3+
__pycache__/
4+
tests/
5+
llama-4-researcher.code-workspace

‎Dockerfile‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
FROM condaforge/miniforge3
2+
3+
WORKDIR /app/
4+
COPY *.py /app/
5+
COPY *.sh /app/
6+
COPY *.yml /app/
7+
8+
RUN bash conda_env.sh
9+
10+
CMD ["bash", "run.sh"]

‎README.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Llama4 Researcher
2+
3+
## Turn topics into essays in seconds!
4+
5+
A friendly research companion built on top of Llama 4, powered by [Groq](https://groq.com), [LinkUp](https://linkup.so), [LlamaIndex](https://www.llamaindex.ai), [Gradio](https://gradio.app), [FastAPI](https://fastapi.tiangolo.com) and [Redis](https://redis.io).
6+
7+
### Full documentation coming soon!

‎agent.py‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
from tools import deepsearch, evaluate_context, expand_query, guard_prompt
2+
from llama_index.llms.groq import Groq
3+
from llama_index.core.agent.workflow import FunctionAgent, AgentWorkflow, ToolCall, ToolCallResult
4+
5+
with open("/run/secrets/groq_key", "r") as f:
6+
groq_api_key = f.read()
7+
f.close()
8+
9+
llm = Groq(model="meta-llama/llama-4-scout-17b-16e-instruct", api_key=groq_api_key)
10+
11+
researcher_agent = FunctionAgent(
12+
llm=llm,
13+
name = "ResearcherAgent",
14+
description="An agent that researches the web and creates essays about a given topic based on the information it found",
15+
system_prompt=f"You are the ResearcherAgent. Your task is to search the web for information about a given topic specified by the user, evsaluate the informmation you retrieved, and finally produce an essay about the topic, making sure to always referencing sources.\n\nPlease, before answering, make sure to understand the context in which the user is acting.\n\nYour workflow must be the following:\n\n1. Expand the query that the user provides you with employing the 'expand_query' tool with, as argument, the original user's query.\n2. Now that you have expanded the query into sub-queries, run the 'deepsearch' tool for each of these queries, retrieving context from the web\n3. Once you gathered information from the web for a sub-query, run the 'evaluate_context' tool. This will tell you how relevant the context is and the reasons for the evaluation. You can keep all the contexts that are more than 70% relevant.\n4. After you have gathered all the information, produce an essay about the topic you are given, basing on the collected context and making sure to cite the sources.\n\nOnce you are done, close the workflow and return the essay to the user.\n\nIMPORTANT INSTRUCTIONS:\n\n- You MUST ALWAYS evaluate the context retrieved from the web",
16+
tools = [expand_query, deepsearch, evaluate_context],
17+
)
18+
19+
workflow = AgentWorkflow(agents = [researcher_agent], root_agent=researcher_agent.name)
20+

‎api.py‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
from agent import workflow, guard_prompt, ToolCall, ToolCallResult
2+
import redis.asyncio as redis
3+
from contextlib import asynccontextmanager
4+
from fastapi import Depends, FastAPI, Header, HTTPException
5+
from fastapi_limiter import FastAPILimiter
6+
from fastapi_limiter.depends import RateLimiter
7+
import json
8+
from fastapi.responses import ORJSONResponse
9+
from pydantic import BaseModel
10+
import gradio as gr
11+
import requests
12+
13+
class ApiInput(BaseModel):
14+
prompt: str
15+
16+
class ApiOutput(BaseModel):
17+
is_safe_prompt: bool
18+
response: str
19+
process: str
20+
21+
with open("/run/secrets/internal_key", "r") as f:
22+
internal_key = f.read()
23+
f.close()
24+
25+
@asynccontextmanager
26+
async def lifespan(_: FastAPI):
27+
redis_connection = redis.from_url("redis://llama_redis:6379", encoding="utf8")
28+
await FastAPILimiter.init(redis_connection)
29+
yield
30+
await FastAPILimiter.close()
31+
32+
async def check_api_key(x_api_key: str = Header(None)):
33+
if x_api_key == internal_key:
34+
return x_api_key
35+
else:
36+
raise HTTPException(status_code=401, detail="Invalid API key")
37+
38+
app = FastAPI(default_response_class=ORJSONResponse, lifespan=lifespan)
39+
40+
@app.get("/test", dependencies=[Depends(RateLimiter(times=10, seconds=1))])
41+
async def index():
42+
return {"response": "Hello world!"}
43+
44+
@app.post("/chat", dependencies=[Depends(RateLimiter(times=10, seconds=60))])
45+
async def chat(inpt: ApiInput, x_api_key: str = Depends(check_api_key)) -> ApiOutput:
46+
is_safe, r = await guard_prompt(inpt.prompt)
47+
process = ""
48+
if not is_safe:
49+
return ApiOutput(is_safe_prompt=is_safe, response="I cannot produce an essay about this topic", process=r)
50+
handler = workflow.run(user_msg=inpt.prompt)
51+
async for event in handler.stream_events():
52+
if isinstance(event, ToolCall):
53+
process += "Calling tool **" + event.tool_name + "**" + " with arguments:\n```json\n" + json.dumps(event.tool_kwargs, indent=4) + "\n```\n\n"
54+
if isinstance(event, ToolCallResult):
55+
process += f"Tool call result for **{event.tool_name}**: {event.tool_output}\n\n"
56+
response = await handler
57+
r = str(response)
58+
return ApiOutput(is_safe_prompt=is_safe, response=r, process=process)
59+
60+
61+
def add_message(history: list, message: dict):
62+
if message is not None:
63+
history.append({"role": "user", "content": message})
64+
return history, gr.Textbox(value=None, interactive=False)
65+
66+
def bot(history: list):
67+
headers = {"Content-Type": "application/json", "x-api-key": internal_key}
68+
response = requests.post("http://localhost:80/chat", json=ApiInput(prompt=history[-1]["content"]).model_dump(), headers=headers)
69+
if response.status_code == 200:
70+
res = response.json()["response"]
71+
process = response.json()["process"]
72+
history.append({"role": "assistant", "content": f"## Agentic Process\n\n{process}"})
73+
return history, "# Canvas\n\n---\n\n"+res
74+
elif response.status_code == 429:
75+
res = "Sorry, we are having high traffic at the moment... Try again later!"
76+
history.append({"role": "assistant", "content": f"Sorry, we are having high traffic at the moment... Try again later!"})
77+
return history, "# Canvas\n\n---\n\n"+res
78+
else:
79+
res = "Sorry, an internal error occurred. Feel free to report the bug on [GitHub discussions](https://github.com/AstraBert/llama-4-researcher/discussions/)"
80+
history.append({"role": "assistant", "content": f"Sorry, an internal error occurred. Feel free to report the bug on [GitHub discussions](https://github.com/AstraBert/llama-4-researcher/discussions/)"})
81+
return history, "# Canvas\n\n---\n\n"+res
82+
83+
with gr.Blocks(theme=gr.themes.Citrus(), title="LlamaResearcher") as frontend:
84+
title = gr.HTML("<h1 align='center'>LlamaResearcher</h1>\n<h2 align='center'>From topic to essay in seconds!</h2>")
85+
with gr.Row():
86+
with gr.Column():
87+
canvas = gr.Markdown(label="Canvas", show_label=True, show_copy_button=True, container=True, min_height=700)
88+
with gr.Column():
89+
chatbot = gr.Chatbot(elem_id="chatbot", type="messages", min_height=700, min_width=700, label="LlamaResearcher Chat")
90+
with gr.Row():
91+
chat_input = gr.Textbox(
92+
interactive=True,
93+
placeholder="Enter message...",
94+
show_label=False,
95+
submit_btn=True,
96+
stop_btn=True,
97+
)
98+
99+
chat_msg = chat_input.submit(
100+
add_message, [chatbot, chat_input], [chatbot, chat_input]
101+
)
102+
bot_msg = chat_msg.then(bot, chatbot, [chatbot, canvas], api_name="bot_response")
103+
bot_msg.then(lambda: gr.Textbox(interactive=True), None, [chat_input])
104+
105+
app = gr.mount_gradio_app(app, frontend, "")

‎compose.yaml‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: llama-4-researcher
2+
3+
services:
4+
llama_redis:
5+
image: redis
6+
ports:
7+
- 6379:6379
8+
networks:
9+
- nginxproxymanager_default
10+
llama_app:
11+
build:
12+
context: .
13+
dockerfile: Dockerfile
14+
secrets:
15+
- groq_key
16+
- internal_key
17+
- linkup_key
18+
networks:
19+
- nginxproxymanager_default
20+
ports:
21+
- 7998:80
22+
23+
networks:
24+
nginxproxymanager_default:
25+
external: true
26+
27+
secrets:
28+
groq_key:
29+
environment: groq_api_key
30+
linkup_key:
31+
environment: linkup_api_key
32+
internal_key:
33+
environment: internal_api_key

‎conda_env.sh‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
eval "$(conda shell.bash hook)"
2+
3+
conda env create -f /app/environment.yml

‎environment.yml‎

Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
name: llama-4-researcher
2+
channels:
3+
- conda-forge
4+
dependencies:
5+
- _libgcc_mutex=0.1=conda_forge
6+
- _openmp_mutex=4.5=2_gnu
7+
- bzip2=1.0.8=h4bc722e_7
8+
- ca-certificates=2025.1.31=hbcca054_0
9+
- ld_impl_linux-64=2.43=h712a8e2_4
10+
- libexpat=2.7.0=h5888daf_0
11+
- libffi=3.4.6=h2dba641_1
12+
- libgcc=14.2.0=h767d61c_2
13+
- libgcc-ng=14.2.0=h69a702a_2
14+
- libgomp=14.2.0=h767d61c_2
15+
- liblzma=5.8.1=hb9d3cd8_0
16+
- libnsl=2.0.1=hd590300_0
17+
- libsqlite=3.49.1=hee588c1_2
18+
- libuuid=2.38.1=h0b41bf4_0
19+
- libxcrypt=4.4.36=hd590300_1
20+
- libzlib=1.3.1=hb9d3cd8_2
21+
- ncurses=6.5=h2d0b736_3
22+
- openssl=3.4.1=h7b32b05_0
23+
- pip=25.0.1=pyh8b19718_0
24+
- python=3.11.11=h9e4cc4f_2_cpython
25+
- readline=8.2=h8c095d6_2
26+
- setuptools=78.1.0=pyhff2d567_0
27+
- tk=8.6.13=noxft_h4845f30_101
28+
- wheel=0.45.1=pyhd8ed1ab_1
29+
- pip:
30+
- aiofiles==23.2.1
31+
- aiohappyeyeballs==2.6.1
32+
- aiohttp==3.11.16
33+
- aiosignal==1.3.2
34+
- annotated-types==0.7.0
35+
- anyio==4.9.0
36+
- attrs==25.3.0
37+
- banks==2.1.1
38+
- beautifulsoup4==4.13.3
39+
- certifi==2025.1.31
40+
- charset-normalizer==3.4.1
41+
- click==8.1.8
42+
- colorama==0.4.6
43+
- dataclasses-json==0.6.7
44+
- deprecated==1.2.18
45+
- dirtyjson==1.0.8
46+
- distro==1.9.0
47+
- dnspython==2.7.0
48+
- email-validator==2.2.0
49+
- fastapi==0.115.12
50+
- fastapi-cli==0.0.7
51+
- fastapi-limiter==0.1.6
52+
- ffmpy==0.5.0
53+
- filelock==3.18.0
54+
- filetype==1.2.0
55+
- frozenlist==1.5.0
56+
- fsspec==2025.3.2
57+
- gradio==5.23.3
58+
- gradio-client==1.8.0
59+
- greenlet==3.1.1
60+
- griffe==1.7.2
61+
- groovy==0.1.2
62+
- h11==0.14.0
63+
- httpcore==1.0.7
64+
- httptools==0.6.4
65+
- httpx==0.28.1
66+
- huggingface-hub==0.30.1
67+
- idna==3.10
68+
- jinja2==3.1.6
69+
- jiter==0.9.0
70+
- joblib==1.4.2
71+
- linkup-sdk==0.2.4
72+
- llama-cloud==0.1.17
73+
- llama-cloud-services==0.6.9
74+
- llama-index==0.12.28
75+
- llama-index-agent-openai==0.4.6
76+
- llama-index-cli==0.4.1
77+
- llama-index-core==0.12.28
78+
- llama-index-embeddings-openai==0.3.1
79+
- llama-index-indices-managed-llama-cloud==0.6.11
80+
- llama-index-llms-groq==0.3.1
81+
- llama-index-llms-openai==0.3.30
82+
- llama-index-llms-openai-like==0.3.4
83+
- llama-index-multi-modal-llms-openai==0.4.3
84+
- llama-index-program-openai==0.3.1
85+
- llama-index-question-gen-openai==0.3.0
86+
- llama-index-readers-file==0.4.7
87+
- llama-index-readers-llama-parse==0.4.0
88+
- llama-parse==0.6.4.post1
89+
- markdown-it-py==3.0.0
90+
- markupsafe==3.0.2
91+
- marshmallow==3.26.1
92+
- mdurl==0.1.2
93+
- multidict==6.3.2
94+
- mypy-extensions==1.0.0
95+
- nest-asyncio==1.6.0
96+
- networkx==3.4.2
97+
- nltk==3.9.1
98+
- numpy==2.2.4
99+
- openai==1.70.0
100+
- orjson==3.10.16
101+
- packaging==24.2
102+
- pandas==2.2.3
103+
- pillow==11.1.0
104+
- platformdirs==4.3.7
105+
- propcache==0.3.1
106+
- pydantic==2.11.2
107+
- pydantic-core==2.33.1
108+
- pydub==0.25.1
109+
- pygments==2.19.1
110+
- pypdf==5.4.0
111+
- python-dateutil==2.9.0.post0
112+
- python-dotenv==1.1.0
113+
- python-multipart==0.0.20
114+
- pytz==2025.2
115+
- pyyaml==6.0.2
116+
- redis==5.2.1
117+
- regex==2024.11.6
118+
- requests==2.32.3
119+
- rich==14.0.0
120+
- rich-toolkit==0.14.1
121+
- ruff==0.11.4
122+
- safehttpx==0.1.6
123+
- safetensors==0.5.3
124+
- semantic-version==2.10.0
125+
- shellingham==1.5.4
126+
- six==1.17.0
127+
- sniffio==1.3.1
128+
- soupsieve==2.6
129+
- sqlalchemy==2.0.40
130+
- starlette==0.46.1
131+
- striprtf==0.0.26
132+
- tenacity==9.1.2
133+
- tiktoken==0.9.0
134+
- tokenizers==0.21.1
135+
- tomlkit==0.13.2
136+
- tqdm==4.67.1
137+
- transformers==4.51.0
138+
- typer==0.15.2
139+
- typing-extensions==4.13.1
140+
- typing-inspect==0.9.0
141+
- typing-inspection==0.4.0
142+
- tzdata==2025.2
143+
- urllib3==2.3.0
144+
- uv==0.6.12
145+
- uvicorn==0.34.0
146+
- uvloop==0.21.0
147+
- watchfiles==1.0.4
148+
- websockets==15.0.1
149+
- wrapt==1.17.2
150+
- yarl==1.19.0

‎run.sh‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
eval "$(conda shell.bash hook)"
2+
3+
conda activate llama-4-researcher
4+
cd /app/
5+
uvicorn api:app --host 0.0.0.0 --port 80
6+
conda deactivate

0 commit comments

Comments
 (0)