Skip to content
This repository was archived by the owner on Jun 3, 2026. It is now read-only.

Commit b124f5c

Browse files
authored
Merge pull request #4 from XortexAI/feat/classifier
Migration: Phase 4.1, Part of Phase 5.2, Part of Phase 6, Part of Phase 8, Phase 9.3
2 parents f90061c + 90237be commit b124f5c

21 files changed

Lines changed: 989 additions & 4 deletions

.gitignore

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,47 @@
1-
.env
1+
# Python
2+
*.py[cod]
3+
__pycache__/
4+
*.so
5+
.Python
6+
build/
7+
develop-eggs/
8+
dist/
9+
downloads/
10+
eggs/
11+
.eggs/
12+
lib/
13+
lib64/
14+
parts/
15+
sdist/
16+
var/
17+
wheels/
18+
share/python-wheels/
19+
*.egg-info/
20+
.installed.cfg
21+
*.egg
22+
MANIFEST
23+
24+
# Virtual environments
25+
venv/
26+
.venv/
27+
env/
28+
.env
29+
30+
# Pytest
31+
.pytest_cache/
32+
.coverage
33+
htmlcov/
34+
coverage.xml
35+
36+
# IDEs
37+
.idea/
38+
.vscode/
39+
*.swp
40+
*.swo
41+
42+
# MacOS
43+
.DS_Store
44+
45+
# Logs
46+
logs/
47+
*.log

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ name = "Xmem"
77
version = "0.1.0"
88
description = "Universal unified memory system for AI agents"
99
authors = [
10-
{name = "Vedant Mahajan", email = "xmemlabs@gmail.com"}
10+
{name = "Vedant Mahajan", email = "xmemlabs@gmail.com"},
1111
{name = "Ishaan Gupta", email = "xmemlabs@gmail.com"}
1212
]
1313
readme = "README.md"

src/agents/__init__.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""Xmem agents — re-export the public agent classes."""
2+
3+
from src.agents.classifier import ClassifierAgent
4+
5+
__all__ = [
6+
"ClassifierAgent",
7+
]

src/agents/base.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import logging
2+
from abc import ABC, abstractmethod
3+
from typing import Any, Dict
4+
5+
from langchain_core.language_models import BaseChatModel
6+
7+
8+
class BaseAgent(ABC):
9+
def __init__(self, model: BaseChatModel, name: str, system_prompt: str = ""):
10+
self.model = model
11+
self.name = name
12+
self.system_prompt = system_prompt
13+
self.logger = logging.getLogger(f"xmem.agents.{name}")
14+
15+
@abstractmethod
16+
async def arun(self, state: Dict[str, Any]) -> Any:
17+
...
18+
19+
def run(self, state: Dict[str, Any]) -> Any:
20+
import asyncio
21+
return asyncio.run(self.arun(state))
22+
23+
def _build_messages(self, user_message: str) -> list:
24+
messages = []
25+
if self.system_prompt:
26+
messages.append({"role": "system", "content": self.system_prompt})
27+
messages.append({"role": "user", "content": user_message})
28+
return messages
29+
30+
async def _call_model(self, messages: list) -> str:
31+
response = await self.model.ainvoke(messages)
32+
content = response.content
33+
if isinstance(content, list):
34+
content = "\n".join(str(c) for c in content)
35+
return content

src/agents/classifier.py

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""
2+
Classifier Agent — the entry-point router for Xmem.
3+
4+
Classifies user input into one or more intent categories (code, profile,
5+
event) so downstream agents only receive the sub-queries relevant to them.
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from typing import Any, Dict
11+
12+
from langchain_core.language_models import BaseChatModel
13+
14+
from src.agents.base import BaseAgent
15+
from src.prompts.classifier import build_system_prompt, pack_classification_query
16+
from src.schemas.classification import ClassificationResult
17+
from src.utils.text import parse_raw_response_to_classifications
18+
19+
20+
class ClassifierAgent(BaseAgent):
21+
def __init__(self, model: BaseChatModel) -> None:
22+
super().__init__(
23+
model=model,
24+
name="classifier",
25+
system_prompt=build_system_prompt(),
26+
)
27+
28+
async def arun(self, state: Dict[str, Any]) -> ClassificationResult:
29+
user_input = state.get("user_query")
30+
if not user_input:
31+
self.logger.debug("Empty query — returning empty classifications.")
32+
return ClassificationResult(classifications=[])
33+
34+
user_message = pack_classification_query(user_input)
35+
messages = self._build_messages(user_message)
36+
raw_content = await self._call_model(messages)
37+
classifications = parse_raw_response_to_classifications(raw_content)
38+
39+
if classifications:
40+
self.logger.info("=" * 50)
41+
self.logger.info("Extracted Classifications:")
42+
for idx, cls in enumerate(classifications, 1):
43+
self.logger.info(
44+
" %d. source=%s query=%s", idx, cls["source"], cls["query"]
45+
)
46+
self.logger.info("Total classifications: %d", len(classifications))
47+
self.logger.info("=" * 50)
48+
else:
49+
self.logger.info("No actionable classifications found (trivial input).")
50+
51+
return ClassificationResult(classifications=classifications)

src/config/constants.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
"""
2+
Shared constants used across Xmem agents and prompt formatting.
3+
4+
These are protocol-level values that all agents rely on for structured
5+
communication with the LLM. Changing them requires updating every
6+
system prompt that references the separator format.
7+
"""
8+
9+
# Delimiter used in the tab-separated format between LLM and agents.
10+
# Format in prompts: `- SOURCE::QUERY`
11+
# Must stay in sync with all system prompts and parsing utilities.
12+
LLM_TAB_SEPARATOR: str = "::"

src/config/settings.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional,List
1+
from typing import Optional, List
22
from pydantic import Field,field_validator
33
from pydantic_settings import BaseSettings,SettingsConfigDict
44

@@ -122,7 +122,6 @@ class Settings(BaseSettings):
122122
@field_validator("fallback_order")
123123
@classmethod
124124
def validate_fallback_order(cls, v: List[str]) -> List[str]:
125-
"""Ensure fallback_order only contains valid provider names."""
126125
valid_providers = {"gemini", "claude", "openai"}
127126
for provider in v:
128127
if provider not in valid_providers:

src/models/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
"""Xmem models — re-export the public API."""
2+
3+
from src.models.base import Provider
4+
from src.models.registry import get_model
5+
6+
__all__ = ["get_model", "Provider"]

src/models/base.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
"""
2+
Base types for the models module.
3+
"""
4+
5+
from typing import Literal
6+
7+
Provider = Literal["gemini", "claude", "openai"]

src/models/claude.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
"""
2+
Claude model factory.
3+
"""
4+
5+
from langchain_anthropic import ChatAnthropic
6+
from langchain_core.language_models import BaseChatModel
7+
8+
from src.config import settings
9+
10+
11+
def build_claude_model(
12+
model_name: str | None = None,
13+
temperature: float | None = None,
14+
) -> BaseChatModel:
15+
api_key = settings.claude_api_key
16+
if not api_key:
17+
raise ValueError("CLAUDE_API_KEY is not set")
18+
19+
return ChatAnthropic(
20+
model=model_name or settings.claude_model,
21+
api_key=api_key,
22+
temperature=temperature if temperature is not None else settings.temperature,
23+
)

0 commit comments

Comments
 (0)