Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions examples/collaborative-agents/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.env
__pycache__/
*.pyc
243 changes: 243 additions & 0 deletions examples/collaborative-agents/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
# Bindu Collaborative Agents

A working multi-agent system demonstrating the **Internet of Agents** using the Bindu framework.

Three specialized agents collaborate to answer questions — each with its own DID identity,
communicating over the A2A protocol.

---

## System Architecture

```
User Query
Coordinator Agent (port 3775)
↓ ↓
Memory Agent Research Agent
(port 3774) (port 3773)
↓ ↓
Semantic DuckDuckGo
Retrieval Web Search
```

**Flow:**
1. User sends query to Coordinator
2. Coordinator checks Memory Agent (semantic similarity search)
3. If found in memory → return instantly
4. If not found → Research Agent searches the web
5. Answer stored in Memory Agent for future queries
6. Response returned to user

---

## Agents

### Research Agent (port 3773)
Searches the web using DuckDuckGo and answers questions using an LLM via OpenRouter.
Specialized in the Bindu AI framework and Internet of Agents concepts.

### Memory Agent (port 3774)
Stores and retrieves knowledge using vector embeddings and cosine similarity.
Uses `text-embedding-3-small` via OpenRouter. Threshold of 0.75 similarity
ensures only relevant memories are returned.

Commands:
- `store:<text>` — stores text in semantic memory
- `retrieve:<query>` — retrieves most similar memory

### Coordinator Agent (port 3775)
Orchestrates the other two agents. Checks memory first, falls back to research,
stores new knowledge automatically. Each call uses the A2A `message/send` protocol.

---

## Prerequisites

- Python 3.12+
- OpenRouter API key (free tier works): https://openrouter.ai

---

## Installation

```bash
git clone https://github.com/Subhajitdas99/bindu-collaborative-agents.git
cd bindu-collaborative-agents
pip install -r requirements.txt
```

Set your API key:

```bash
# Linux/macOS
export OPENROUTER_API_KEY="your-api-key"

# Windows PowerShell
$env:OPENROUTER_API_KEY="your-api-key"
```

Or create a `.env` file:

```
OPENROUTER_API_KEY=your-api-key
BINDU_AUTHOR=your.email@example.com
```

---

## Running

Open **3 separate terminals** and run one agent in each:

**Terminal 1 — Research Agent**
```bash
python research_agent.py
```

**Terminal 2 — Memory Agent**
```bash
python memory_agent.py
```

**Terminal 3 — Coordinator Agent**
```bash
python coordinator_agent.py
```

---

## Testing

Send a query to the coordinator (Terminal 4):

**Linux/macOS:**
```bash
curl -X POST http://localhost:3775/ \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{"kind": "text", "text": "What is Bindu?"}],
"kind": "message",
"messageId": "11111111-1111-1111-1111-111111111111",
"contextId": "11111111-1111-1111-1111-111111111112",
"taskId": "11111111-1111-1111-1111-111111111113"
},
"configuration": {"acceptedOutputModes": ["application/json"]}
},
"id": "11111111-1111-1111-1111-111111111114"
}'
```

**Windows PowerShell:**
```powershell
Invoke-WebRequest -Uri "http://localhost:3775/" `
-Method POST `
-ContentType "application/json" `
-UseBasicParsing `
-Body '{"jsonrpc":"2.0","method":"message/send","params":{"message":{"role":"user","parts":[{"kind":"text","text":"What is Bindu?"}],"kind":"message","messageId":"11111111-1111-1111-1111-111111111111","contextId":"11111111-1111-1111-1111-111111111112","taskId":"11111111-1111-1111-1111-111111111113"},"configuration":{"acceptedOutputModes":["application/json"]}},"id":"11111111-1111-1111-1111-111111111114"}' | Select-Object -ExpandProperty Content
```

Wait 15-20 seconds (research takes time), then poll for the result:

**Linux/macOS:**
```bash
curl -X POST http://localhost:3775/ \
-H "Content-Type: application/json" \
-d '{
"jsonrpc": "2.0",
"method": "tasks/get",
"params": {"taskId": "11111111-1111-1111-1111-111111111113"},
"id": "11111111-1111-1111-1111-111111111115"
}'
```

**Windows PowerShell:**
```powershell
Invoke-WebRequest -Uri "http://localhost:3775/" `
-Method POST `
-ContentType "application/json" `
-UseBasicParsing `
-Body '{"jsonrpc":"2.0","method":"tasks/get","params":{"taskId":"11111111-1111-1111-1111-111111111113"},"id":"11111111-1111-1111-1111-111111111115"}' | Select-Object -ExpandProperty Content
```

---

## Expected Output

First query (cache miss — researches the web):
```json
{
"result": {
"status": {"state": "completed"},
"artifacts": [{
"parts": [{
"kind": "text",
"text": "The Bindu AI Framework is designed to facilitate the creation of
intelligent and interoperable AI agents..."
}]
}]
}
}
```

Second identical query (cache hit — served from memory instantly):
```json
{
"result": {
"artifacts": [{
"parts": [{"kind": "text", "text": "(From Memory) The Bindu AI Framework..."}]
}]
}
}
```

---

## What This Demonstrates

- **Agent-to-Agent (A2A) communication** over HTTP using the Bindu protocol
- **DID identity** — each agent gets a unique Decentralized Identifier
- **Semantic memory** — knowledge retrieved by meaning, not keyword matching
- **Coordinator pattern** — orchestration without tight coupling
- **Internet of Agents** — agents discovering and calling each other

---

## Project Structure

```
collaborative-agents/
├── coordinator_agent.py — orchestrates research + memory agents
├── research_agent.py — web search via DuckDuckGo
├── memory_agent.py — semantic memory store/retrieve
├── requirements.txt
├── .env — API keys (not committed)
├── .gitignore
└── utils/
└── semantic_memory.py — embeddings + cosine similarity
```

---

## Technologies

- [Bindu](https://github.com/getbindu/bindu) — Internet of Agents framework
- [Agno](https://github.com/agno-agi/agno) — agent framework
- [OpenRouter](https://openrouter.ai) — LLM + embeddings API
- [DuckDuckGo](https://pypi.org/project/duckduckgo-search/) — web search
- NumPy — cosine similarity computation

---

## Author

**Subhajit Das**
Final Year B.Tech AI/ML Student
Interested in Multi-Agent Systems, AI Infrastructure, and Autonomous Agents.

GitHub: [@Subhajitdas99](https://github.com/Subhajitdas99)
117 changes: 117 additions & 0 deletions examples/collaborative-agents/coordinator_agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
"""Coordinator agent — orchestrates research and memory agents."""
from dotenv import load_dotenv
load_dotenv()
import os
import time

import httpx
from bindu.penguin.bindufy import bindufy

MEMORY_AGENT_URL = os.getenv("MEMORY_AGENT_URL", "http://localhost:3774")
RESEARCH_AGENT_URL = os.getenv("RESEARCH_AGENT_URL", "http://localhost:3773")

import uuid


def call_agent(url: str, message: str) -> str:
"""Send a message to another Bindu agent, poll until complete, return result."""
msg_id = str(uuid.uuid4())
ctx_id = str(uuid.uuid4())
task_id = str(uuid.uuid4())
rpc_id = str(uuid.uuid4())

payload = {
"jsonrpc": "2.0",
"method": "message/send",
"params": {
"message": {
"role": "user",
"parts": [{"kind": "text", "text": message}],
"kind": "message",
"messageId": msg_id,
"contextId": ctx_id,
"taskId": task_id,
},
"configuration": {"acceptedOutputModes": ["application/json"]},
},
"id": rpc_id,
}

# Send the message
response = httpx.post(url, json=payload, timeout=30)
result = response.json()
actual_task_id = result.get("result", {}).get("id")
if not actual_task_id:
return "No response from agent"

# Poll until completed
for _ in range(30):
time.sleep(1)
poll_payload = {
"jsonrpc": "2.0",
"method": "tasks/get",
"params": {"taskId": actual_task_id},
"id": str(uuid.uuid4()),
}
poll_response = httpx.post(url, json=poll_payload, timeout=10)
poll_result = poll_response.json().get("result", {})
state = poll_result.get("status", {}).get("state")

if state == "completed":
artifacts = poll_result.get("artifacts", [])
if artifacts:
parts = artifacts[0].get("parts", [])
if parts:
text = parts[0].get("text", "")
# Don't return error responses
if text and "No response from agent" not in text:
return text
return "No response from agent"

return "Agent timed out"


def handler(messages: list[dict]) -> str:
"""Coordinate memory and research agents to answer a query."""
query = messages[-1]["content"]

# 1. Try memory first
memory_result = call_agent(MEMORY_AGENT_URL, f"retrieve:{query}")
if memory_result and memory_result not in (
"No relevant memory found.",
"No response from agent",
"Agent timed out",
):
print("Retrieved from memory.")
return f"(From Memory) {memory_result}"

# 2. Fall back to research
print("Researching...")
research_result = call_agent(RESEARCH_AGENT_URL, query)

# 3. Store only valid results
if research_result and research_result not in (
"No response from agent",
"Agent timed out",
):
call_agent(MEMORY_AGENT_URL, f"store:{research_result}")

return research_result


config = {
"author": os.getenv("BINDU_AUTHOR", "your.email@example.com"),
"name": "coordinator_agent",
"description": (
"Coordinates research and memory agents to answer questions "
"about the Bindu framework."
),
"deployment": {
"url": os.getenv("BINDU_DEPLOYMENT_URL", "http://localhost:3775"),
"expose": True,
},
"skills": [],
}

if __name__ == "__main__":
bindufy(config, handler)
Loading