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
1 change: 1 addition & 0 deletions api/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""HTTP API package for serving GMemory as an external memory backend."""
61 changes: 61 additions & 0 deletions api/prompt_renderer.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
from mas.memory.common import MASMessage


TASK_SOLVE_WITH_INSIGHTS = """
## Successful Examples (Reference Cases)
Below are some examples of similar tasks that were successfully completed.
Please use these as references to guide your thinking and approach to the current task:

{few_shots}
---

## Your Own Past Successes (Execution Patterns)
Here are examples of successful execution processes you've previously used on similar tasks.
Pay special attention to the step-by-step procedures and strategies, especially when encountering obstacles:

{memory_few_shots}
---

## Key Insights from Related Tasks
The following are insights gathered during the execution of similar tasks. You may refer to them during your task execution to improve problem-solving accuracy.

{insights}
---

## Your Turn: Take Action!
Use the above examples and insights as a foundation, and now work on the following task:
{task_description}
"""

TASK_CONTEXT = """
### Task description:
{task_description}

### Key steps:
{key_steps}

### Detailed trajectory:
{trajectory}
"""


def render_memory_prompt(successful: list[MASMessage], insights: list[str], task_description: str) -> str:
if not successful and not insights:
return ""

memory_few_shots = "\n\n".join(
f"Task {idx + 1}:\n"
+ TASK_CONTEXT.format(
task_description=item.task_description,
key_steps=item.get_extra_field("key_steps"),
trajectory=item.task_trajectory,
)
for idx, item in enumerate(successful)
)
insight_text = "\n".join(f"{idx}. {insight}" for idx, insight in enumerate(insights, 1))
return TASK_SOLVE_WITH_INSIGHTS.format(
few_shots="",
memory_few_shots=memory_few_shots,
insights=insight_text,
task_description=task_description,
)
66 changes: 66 additions & 0 deletions api/schemas.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
from typing import Any, Optional

from pydantic import BaseModel, ConfigDict, Field, StrictBool


class EpisodeStep(BaseModel):
subgoal: Optional[str] = None
action: str
observation: str
reward: float = 0.0


class RetrieveRequest(BaseModel):
task_type: str
goal: str
initial_observation: str
max_chars: int = Field(default=4000, gt=0)
metadata: dict[str, Any] = Field(default_factory=dict)


class EpisodeRequest(BaseModel):
task_type: str
goal: str
initial_observation: str
success: StrictBool
progress_rate: Optional[float] = None
steps: list[EpisodeStep] = Field(default_factory=list)
metadata: dict[str, Any] = Field(default_factory=dict)


class MemoryStats(BaseModel):
memory_size: int
successful_count: int
failed_count: int
insight_count: int


class RetrieveResponse(BaseModel):
memory_prompt: str
stats: MemoryStats
trace_id: str
error: Optional[str] = None


class EpisodeResponse(BaseModel):
stored: bool
episode_id: Optional[str] = None
trace_id: str
error: Optional[str] = None


class HealthResponse(BaseModel):
ok: bool
backend: str
namespace: str
memory_size: int
error: Optional[str] = None


class TraceArtifact(BaseModel):
model_config = ConfigDict(arbitrary_types_allowed=True)

request: dict[str, Any]
derived: dict[str, Any]
response: dict[str, Any]
error: Optional[str] = None
54 changes: 54 additions & 0 deletions api/server.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import os

from dotenv import load_dotenv
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.encoders import jsonable_encoder
from fastapi.responses import JSONResponse

load_dotenv()
os.environ.setdefault("OPENAI_API_BASE", "")
os.environ.setdefault("OPENAI_API_KEY", "")

from .schemas import EpisodeRequest, HealthResponse, RetrieveRequest
from .service import GMemoryApiService


app = FastAPI(title="GMemory API", version="0.1.0")
service = GMemoryApiService()


@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
trace_id = service.tracer.new_trace_id()
errors = jsonable_encoder(exc.errors())
error = f"RequestValidationError: {errors}"
response = {"detail": errors, "trace_id": trace_id, "error": error}
try:
body = await request.json()
except Exception:
body = {}
service.tracer.record(
trace_id,
request.url.path,
body,
{"validation_error": True},
response,
error,
)
return JSONResponse(status_code=422, content=response)


@app.get("/api/v1/memory/health", response_model=HealthResponse)
def health():
return service.health()


@app.post("/api/v1/memory/retrieve")
def retrieve_memory(request: RetrieveRequest):
return service.retrieve(request)


@app.post("/api/v1/memory/episodes")
def save_episode(request: EpisodeRequest):
return service.save_episode(request)
Loading