Skip to content

Commit eb508a0

Browse files
committed
feat(core): add core modules for AI framework
1 parent 0a4f720 commit eb508a0

19 files changed

+915
-0
lines changed

reme_ai/core/context/__init__.py

Whitespace-only changes.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
class BaseContext(dict):
2+
"""A dict subclass that supports attribute-style access and pickling."""
3+
4+
def __getattr__(self, name):
5+
try:
6+
return self[name]
7+
except KeyError:
8+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
9+
10+
def __setattr__(self, name, value):
11+
self[name] = value
12+
13+
def __delattr__(self, name):
14+
try:
15+
del self[name]
16+
except KeyError:
17+
raise AttributeError(f"'{type(self).__name__}' object has no attribute '{name}'")
18+
19+
def __getstate__(self):
20+
return dict(self)
21+
22+
def __setstate__(self, state):
23+
self.update(state)
24+
25+
def __reduce__(self):
26+
return self.__class__, (), self.__getstate__()
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""Flow context for managing flow execution state.
2+
3+
This module provides a context class for managing the state of flow execution,
4+
including flow identification, response handling, and streaming capabilities.
5+
"""
6+
7+
import asyncio
8+
import uuid
9+
from typing import Optional
10+
11+
from .base_context import BaseContext
12+
from ..enumeration import ChunkEnum
13+
from ..schema import FlowResponse
14+
from ..schema import FlowStreamChunk
15+
16+
17+
class FlowContext(BaseContext):
18+
"""Context for managing flow execution state and streaming.
19+
20+
This class manages the state of a single flow execution, including:
21+
- Flow identification
22+
- Response handling
23+
- Stream queue for asynchronous streaming
24+
25+
Attributes:
26+
flow_id: Unique identifier for the flow instance.
27+
response: FlowResponse object for storing flow results.
28+
stream_queue: Asynchronous queue for streaming chunks.
29+
"""
30+
31+
def __init__(
32+
self,
33+
flow_id: str = uuid.uuid4().hex,
34+
response: Optional[FlowResponse] = None,
35+
stream_queue: Optional[asyncio.Queue] = None,
36+
**kwargs,
37+
):
38+
"""Initialize FlowContext with flow ID and optional components.
39+
40+
Args:
41+
flow_id: Unique identifier for the flow instance.
42+
Defaults to a random UUID hex string.
43+
response: FlowResponse object. If None, a new FlowResponse
44+
will be created.
45+
stream_queue: Asynchronous queue for streaming chunks.
46+
**kwargs: Additional context data to store.
47+
"""
48+
super().__init__(**kwargs)
49+
50+
self.flow_id: str = flow_id
51+
self.response: Optional[FlowResponse] = response if response is not None else FlowResponse()
52+
self.stream_queue: Optional[asyncio.Queue] = stream_queue
53+
54+
async def add_stream_string_and_type(self, chunk: str, chunk_type: ChunkEnum):
55+
"""Add a stream chunk with string content and type to the stream queue.
56+
57+
Args:
58+
chunk: The string content to stream.
59+
chunk_type: The type of the chunk.
60+
61+
Returns:
62+
Self for method chaining.
63+
"""
64+
stream_chunk = FlowStreamChunk(flow_id=self.flow_id, chunk_type=chunk_type, chunk=chunk)
65+
await self.stream_queue.put(stream_chunk)
66+
return self
67+
68+
async def add_stream_chunk(self, stream_chunk: FlowStreamChunk):
69+
"""Add a stream chunk to the stream queue.
70+
71+
Args:
72+
stream_chunk: The stream chunk to add.
73+
74+
Returns:
75+
Self for method chaining.
76+
"""
77+
stream_chunk.flow_id = self.flow_id
78+
await self.stream_queue.put(stream_chunk)
79+
return self
80+
81+
async def add_stream_done(self):
82+
"""Add a done signal to the stream queue.
83+
84+
Returns:
85+
Self for method chaining.
86+
"""
87+
done_chunk = FlowStreamChunk(flow_id=self.flow_id, chunk_type=ChunkEnum.DONE, chunk="", done=True)
88+
await self.stream_queue.put(done_chunk)
89+
return self
90+
91+
def add_response_error(self, e: Exception):
92+
"""Add an error to the flow response.
93+
94+
Args:
95+
e: The exception to record as an error.
96+
"""
97+
self.response.success = False
98+
self.response.answer = str(e.args)
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
"""Prompt handler for loading and managing prompts.
2+
3+
This module provides a class for loading prompts from files, managing
4+
language-specific prompts, and formatting prompts with variables.
5+
"""
6+
7+
from pathlib import Path
8+
9+
import yaml
10+
from loguru import logger
11+
12+
from .base_context import BaseContext
13+
from .service_context import C
14+
15+
16+
class PromptHandler(BaseContext):
17+
"""Handler for loading, storing, and formatting prompts.
18+
19+
This class manages prompts loaded from YAML files, supports
20+
language-specific prompts, and provides formatting with variables
21+
and conditional flags.
22+
23+
Attributes:
24+
language: Language code for language-specific prompts.
25+
"""
26+
27+
def __init__(self, language: str = "", **kwargs):
28+
"""Initialize PromptHandler with language setting.
29+
30+
Args:
31+
language: Language code for language-specific prompts.
32+
If not provided, uses the language from service context.
33+
**kwargs: Additional context data to store.
34+
"""
35+
super().__init__(**kwargs)
36+
self.language: str = language or C.language
37+
38+
def load_prompt_by_file(self, prompt_file_path: Path | str = None):
39+
"""Load prompts from a YAML file.
40+
41+
Args:
42+
prompt_file_path: Path to the YAML file containing prompts.
43+
Can be a Path object or string. If None, does nothing.
44+
45+
Returns:
46+
Self for method chaining.
47+
"""
48+
if prompt_file_path is None:
49+
return self
50+
51+
if isinstance(prompt_file_path, str):
52+
prompt_file_path = Path(prompt_file_path)
53+
54+
if not prompt_file_path.exists():
55+
return self
56+
57+
with prompt_file_path.open(encoding="utf-8") as f:
58+
prompt_dict = yaml.load(f, yaml.FullLoader)
59+
self.load_prompt_dict(prompt_dict)
60+
return self
61+
62+
def load_prompt_dict(self, prompt_dict: dict = None):
63+
"""Load prompts from a dictionary.
64+
65+
Args:
66+
prompt_dict: Dictionary of prompt names to prompt strings.
67+
If None, does nothing.
68+
69+
Returns:
70+
Self for method chaining.
71+
"""
72+
if not prompt_dict:
73+
return self
74+
75+
for key, value in prompt_dict.items():
76+
if isinstance(value, str):
77+
if key in self._data:
78+
self._data[key] = value
79+
logger.warning(f"prompt_dict key={key} overwrite!")
80+
81+
else:
82+
self._data[key] = value
83+
logger.debug(f"add prompt_dict key={key}")
84+
return self
85+
86+
def get_prompt(self, prompt_name: str):
87+
"""Get a prompt by name, with language suffix if applicable.
88+
89+
Args:
90+
prompt_name: Base name of the prompt to retrieve.
91+
92+
Returns:
93+
The prompt string.
94+
95+
Raises:
96+
AssertionError: If the prompt (with language suffix) is not found.
97+
"""
98+
key: str = prompt_name
99+
if self.language and not key.endswith(self.language.strip()):
100+
key += "_" + self.language.strip()
101+
102+
assert key in self._data, f"prompt_name={key} not found."
103+
return self._data[key]
104+
105+
def prompt_format(self, prompt_name: str, **kwargs) -> str:
106+
"""Format a prompt with variables and conditional flags.
107+
108+
This method supports two types of formatting:
109+
1. Boolean flags: Lines starting with [flag_name] are included
110+
only if the corresponding flag is True.
111+
2. Variable substitution: Other kwargs are used for string
112+
formatting with {variable_name}.
113+
114+
Args:
115+
prompt_name: Name of the prompt to format.
116+
**kwargs: Variables and flags for formatting.
117+
118+
Returns:
119+
The formatted prompt string.
120+
"""
121+
prompt = self.get_prompt(prompt_name)
122+
123+
flag_kwargs = {k: v for k, v in kwargs.items() if isinstance(v, bool)}
124+
other_kwargs = {k: v for k, v in kwargs.items() if not isinstance(v, bool)}
125+
126+
if flag_kwargs:
127+
split_prompt = []
128+
for line in prompt.strip().split("\n"):
129+
hit = False
130+
hit_flag = True
131+
for key, flag in kwargs.items():
132+
if not line.startswith(f"[{key}]"):
133+
continue
134+
135+
hit = True
136+
hit_flag = flag
137+
line = line.strip(f"[{key}]")
138+
break
139+
140+
if not hit:
141+
split_prompt.append(line)
142+
elif hit_flag:
143+
split_prompt.append(line)
144+
145+
prompt = "\n".join(split_prompt)
146+
147+
if other_kwargs:
148+
prompt = prompt.format(**other_kwargs)
149+
150+
return prompt

reme_ai/core/context/registry.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
"""Registry for class registration and lookup.
2+
3+
This module provides a registry class for managing class registrations
4+
with dynamic lookup capabilities.
5+
"""
6+
7+
from .base_context import BaseContext
8+
9+
10+
class Registry(BaseContext):
11+
"""Registry for storing and retrieving registered classes.
12+
13+
This class provides a decorator-based registration system for classes,
14+
allowing dynamic class lookup by name.
15+
"""
16+
17+
def register(self, name: str = "", add_cls: bool = True):
18+
"""Register a class in the registry.
19+
20+
Args:
21+
name: Name to register the class under.
22+
add_cls: Whether to actually add the class to the registry.
23+
Defaults to True.
24+
25+
Returns:
26+
Decorator function that registers the class when applied.
27+
"""
28+
29+
def decorator(cls):
30+
"""Decorator function that registers the class.
31+
32+
Args:
33+
cls: The class to register.
34+
35+
Returns:
36+
The class unchanged (for use as decorator).
37+
"""
38+
if add_cls:
39+
key = name or cls.__name__
40+
self._data[key] = cls
41+
return cls
42+
43+
return decorator
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from .chunk_enum import ChunkEnum
2+
from .http_enum import HttpEnum
3+
from .registry_enum import RegistryEnum
4+
from .role import Role
5+
6+
__all__ = [
7+
"ChunkEnum",
8+
"HttpEnum",
9+
"RegistryEnum",
10+
"Role",
11+
]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from enum import Enum
2+
3+
4+
class ChunkEnum(str, Enum):
5+
THINK = "think"
6+
7+
ANSWER = "answer"
8+
9+
TOOL = "tool"
10+
11+
USAGE = "usage"
12+
13+
ERROR = "error"
14+
15+
DONE = "done"
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
from enum import Enum
2+
3+
4+
class HttpEnum(str, Enum):
5+
GET = "get"
6+
7+
POST = "post"
8+
9+
HEAD = "head"
10+
11+
PUT = "put"
12+
13+
DELETE = "delete"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from enum import Enum
2+
3+
4+
class RegistryEnum(str, Enum):
5+
LLM = "llm"
6+
7+
EMBEDDING_MODEL = "embedding_model"
8+
9+
VECTOR_STORE = "vector_store"
10+
11+
OP = "op"
12+
13+
FLOW = "flow"
14+
15+
SERVICE = "service"
16+
17+
TOKEN_COUNTER = "token_counter"

reme_ai/core/enumeration/role.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
from enum import Enum
2+
3+
4+
class Role(str, Enum):
5+
SYSTEM = "system"
6+
7+
USER = "user"
8+
9+
ASSISTANT = "assistant"
10+
11+
TOOL = "tool"

0 commit comments

Comments
 (0)