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
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,65 @@ service = MemUService(

---

### OpenRouter Integration

MemU supports [OpenRouter](https://openrouter.ai) as a model provider, giving you access to multiple LLM providers through a single API.

#### Configuration

```python
from memu import MemoryService

service = MemoryService(
llm_profiles={
"default": {
"provider": "openrouter",
"client_backend": "httpx",
"base_url": "https://openrouter.ai",
"api_key": "your_openrouter_api_key",
"chat_model": "anthropic/claude-3.5-sonnet", # Any OpenRouter model
"embed_model": "openai/text-embedding-3-small", # Embedding model
},
},
database_config={
"metadata_store": {"provider": "inmemory"},
},
)
```

#### Environment Variables

| Variable | Description |
|----------|-------------|
| `OPENROUTER_API_KEY` | Your OpenRouter API key from [openrouter.ai/keys](https://openrouter.ai/keys) |

#### Supported Features

| Feature | Status | Notes |
|---------|--------|-------|
| Chat Completions | Supported | Works with any OpenRouter chat model |
| Embeddings | Supported | Use OpenAI embedding models via OpenRouter |
| Vision | Supported | Use vision-capable models (e.g., `openai/gpt-4o`) |

#### Running OpenRouter Tests

```bash
export OPENROUTER_API_KEY=your_api_key

# Full workflow test (memorize + retrieve)
python tests/test_openrouter.py

# Embedding-specific tests
python tests/test_openrouter_embedding.py

# Vision-specific tests
python tests/test_openrouter_vision.py
```

See [`examples/example_4_openrouter_memory.py`](examples/example_4_openrouter_memory.py) for a complete working example.

---

## 📖 Core APIs

### `memorize()` - Extract and Store Memory
Expand Down
112 changes: 112 additions & 0 deletions examples/example_4_gemini_memory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""
Example 5: Multiple Conversations -> Memory Category File (Using Google Gemini)

This example demonstrates how to process multiple conversation files
and generate memory categories using Google Gemini as the LLM backend.

Usage:
export GEMINI_API_KEY=your_api_key
python examples/example_4_gemini_memory.py
"""

import asyncio
import os
import sys

from memu.app import MemoryService

src_path = os.path.abspath("src")
sys.path.insert(0, src_path)


async def generate_memory_md(categories, output_dir):
"""Generate concise markdown files for each memory category."""
os.makedirs(output_dir, exist_ok=True)
generated_files = []

for cat in categories:
name = cat.get("name", "unknown")
summary = cat.get("summary", "")

filename = f"{name}.md"
filepath = os.path.join(output_dir, filename)

with open(filepath, "w", encoding="utf-8") as f:
if summary:
cleaned_summary = summary.replace("<content>", "").replace("</content>", "").strip()
f.write(f"{cleaned_summary}\n")
else:
f.write("*No content available*\n")

generated_files.append(filename)

return generated_files


async def main():
"""
Process multiple conversation files and generate memory categories using Gemini.

This example:
1. Initializes MemoryService with Google Gemini API
2. Processes conversation JSON files
3. Extracts memory categories from conversations
4. Outputs the categories to files
"""
print("Example 4: Conversation Memory Processing (Google Gemini)")
print("-" * 55)

api_key = os.getenv("GEMINI_API_KEY")
if not api_key:
msg = "Please set GEMINI_API_KEY environment variable"
raise ValueError(msg)

# Initialize service with Google Gemini
service = MemoryService(
llm_profiles={
"default": {
"provider": "gemini",
"client_backend": "httpx",
"base_url": "https://generativelanguage.googleapis.com/v1beta",
"api_key": api_key,
"chat_model": "gemini-2.5-pro", # Fast and capable model
"embed_model": "text-embedding-004", # Gemini's embedding model
},
},
)

conversation_files = [
"examples/resources/conversations/conv1.json",
"examples/resources/conversations/conv2.json",
"examples/resources/conversations/conv3.json",
]

print("\nProcessing conversations...")
total_items = 0
categories = []

for conv_file in conversation_files:
if not os.path.exists(conv_file):
print(f"Skipped: {conv_file} not found")
continue

try:
print(f"Processing: {conv_file}")
result = await service.memorize(resource_url=conv_file, modality="conversation")
total_items += len(result.get("items", []))
categories = result.get("categories", [])
except Exception as e:
print(f"Error processing {conv_file}: {e}")

output_dir = "examples/output/gemini_example"
os.makedirs(output_dir, exist_ok=True)

await generate_memory_md(categories, output_dir)

print(f"\nProcessed {len(conversation_files)} files, extracted {total_items} items")
print(f"Generated {len(categories)} categories")
print(f"Output: {output_dir}/")


if __name__ == "__main__":
asyncio.run(main())
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ lint = [
]
test = [
"pytest>=8.4.2",
"pytest-asyncio>=0.24.0",
"pytest-cov>=7.0.0",
]

Expand Down Expand Up @@ -156,3 +157,4 @@ source = ["memu"]
testpaths = ["tests"]
log_cli = true
log_cli_level = "INFO"
asyncio_mode = "auto"
3 changes: 2 additions & 1 deletion src/memu/llm/backends/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from memu.llm.backends.base import LLMBackend
from memu.llm.backends.doubao import DoubaoLLMBackend
from memu.llm.backends.gemini import GeminiLLMBackend
from memu.llm.backends.openai import OpenAILLMBackend

__all__ = ["DoubaoLLMBackend", "LLMBackend", "OpenAILLMBackend"]
__all__ = ["DoubaoLLMBackend", "GeminiLLMBackend", "LLMBackend", "OpenAILLMBackend"]
102 changes: 102 additions & 0 deletions src/memu/llm/backends/gemini.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
from __future__ import annotations

from typing import Any, cast

from memu.llm.backends.base import LLMBackend


class GeminiLLMBackend(LLMBackend):
"""Backend for Google Gemini LLM API.

Gemini uses a different API format than OpenAI-compatible APIs:
- Endpoint: /models/{model}:generateContent
- Auth: x-goog-api-key header
- Content format: contents[].parts[].text
"""

name = "gemini"
summary_endpoint = "/models/{model}:generateContent"

def build_summary_payload(
self, *, text: str, system_prompt: str | None, chat_model: str, max_tokens: int | None
) -> dict[str, Any]:
"""Build payload for Gemini generateContent API."""
contents: list[dict[str, Any]] = []

# Add user message
contents.append({"role": "user", "parts": [{"text": text}]})

payload: dict[str, Any] = {
"contents": contents,
}

# Add system instruction if provided
# Note: When system_prompt is None, we don't set a default to allow the user prompt
# to fully control the output format (e.g., for JSON responses)
if system_prompt:
payload["system_instruction"] = {"parts": [{"text": system_prompt}]}

# Add generation config
generation_config: dict[str, Any] = {
"temperature": 1.0, # Gemini recommends keeping at 1.0
}
if max_tokens is not None:
generation_config["maxOutputTokens"] = max_tokens
payload["generationConfig"] = generation_config

return payload

def parse_summary_response(self, data: dict[str, Any]) -> str:
"""Parse Gemini generateContent response."""
try:
return cast(str, data["candidates"][0]["content"]["parts"][0]["text"])
except (KeyError, IndexError) as e:
msg = f"Failed to parse Gemini response: {e}"
raise ValueError(msg) from e

def build_vision_payload(
self,
*,
prompt: str,
base64_image: str,
mime_type: str,
system_prompt: str | None,
chat_model: str,
max_tokens: int | None,
) -> dict[str, Any]:
"""Build payload for Gemini Vision API with inline image data."""
# Build user content with text and image parts
user_parts: list[dict[str, Any]] = [
{"text": prompt},
{
"inline_data": {
"mime_type": mime_type,
"data": base64_image,
}
},
]

contents: list[dict[str, Any]] = [
{
"role": "user",
"parts": user_parts,
}
]

payload: dict[str, Any] = {
"contents": contents,
}

# Add system instruction if provided
if system_prompt:
payload["system_instruction"] = {"parts": [{"text": system_prompt}]}

# Add generation config
generation_config: dict[str, Any] = {
"temperature": 1.0,
}
if max_tokens is not None:
generation_config["maxOutputTokens"] = max_tokens
payload["generationConfig"] = generation_config

return payload
Loading