Spaces:
Running
Running
feat: implement Redis-backed user state management and update app structure
Browse files- README.md +3 -3
- packages.txt +1 -0
- requirements.txt +4 -1
- src/agent/llm_graph.py +1 -1
- src/agent/redis_state.py +51 -0
- src/agent/runner.py +1 -1
- src/agent/state.py +0 -24
- src/agent/tools.py +1 -1
- src/app.py +44 -0
- src/main.py +3 -2
README.md
CHANGED
@@ -1,12 +1,12 @@
|
|
1 |
---
|
2 |
-
title:
|
3 |
-
emoji: 🎮
|
4 |
colorFrom: yellow
|
5 |
colorTo: green
|
6 |
sdk: gradio
|
7 |
sdk_version: 5.32.0
|
8 |
python_version: "3.11"
|
9 |
-
app_file: src/
|
10 |
pinned: false
|
11 |
license: mit
|
12 |
---
|
|
|
1 |
---
|
2 |
+
title: LLMGameHub
|
3 |
+
emoji: "🎮"
|
4 |
colorFrom: yellow
|
5 |
colorTo: green
|
6 |
sdk: gradio
|
7 |
sdk_version: 5.32.0
|
8 |
python_version: "3.11"
|
9 |
+
app_file: src/app.py
|
10 |
pinned: false
|
11 |
license: mit
|
12 |
---
|
packages.txt
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
redis-server
|
requirements.txt
CHANGED
@@ -7,6 +7,7 @@ asyncio
|
|
7 |
aiohttp==3.9.1
|
8 |
pygame==2.5.2
|
9 |
numpy
|
|
|
10 |
langchain==0.3.17
|
11 |
langchain-core==0.3.58
|
12 |
langchain-community==0.3.16
|
@@ -14,4 +15,6 @@ langchain-google-genai==2.1.4
|
|
14 |
pydantic-core==2.23.4
|
15 |
pydantic-settings==2.7.1
|
16 |
pydantic==2.9.2
|
17 |
-
|
|
|
|
|
|
7 |
aiohttp==3.9.1
|
8 |
pygame==2.5.2
|
9 |
numpy
|
10 |
+
langgraph
|
11 |
langchain==0.3.17
|
12 |
langchain-core==0.3.58
|
13 |
langchain-community==0.3.16
|
|
|
15 |
pydantic-core==2.23.4
|
16 |
pydantic-settings==2.7.1
|
17 |
pydantic==2.9.2
|
18 |
+
redis[hiredis]>=5
|
19 |
+
aioredis>=2
|
20 |
+
msgpack
|
src/agent/llm_graph.py
CHANGED
@@ -14,7 +14,7 @@ from agent.tools import (
|
|
14 |
generate_story_frame,
|
15 |
update_state_with_choice,
|
16 |
)
|
17 |
-
from agent.
|
18 |
from audio.audio_generator import change_music_tone
|
19 |
logger = logging.getLogger(__name__)
|
20 |
|
|
|
14 |
generate_story_frame,
|
15 |
update_state_with_choice,
|
16 |
)
|
17 |
+
from agent.redis_state import get_user_state
|
18 |
from audio.audio_generator import change_music_tone
|
19 |
logger = logging.getLogger(__name__)
|
20 |
|
src/agent/redis_state.py
ADDED
@@ -0,0 +1,51 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
"""Async Redis-backed user state storage."""
|
2 |
+
|
3 |
+
from __future__ import annotations
|
4 |
+
|
5 |
+
import json
|
6 |
+
import msgpack
|
7 |
+
import redis.asyncio as redis
|
8 |
+
|
9 |
+
from agent.models import UserState
|
10 |
+
|
11 |
+
|
12 |
+
class UserRepository:
|
13 |
+
"""Repository for storing UserState objects in Redis."""
|
14 |
+
|
15 |
+
def __init__(self, redis_url: str = "redis://localhost") -> None:
|
16 |
+
self.redis = redis.from_url(redis_url)
|
17 |
+
|
18 |
+
async def get(self, user_id: str) -> UserState:
|
19 |
+
"""Return user state for the given id, creating it if absent."""
|
20 |
+
key = f"llmgamehub:{user_id}"
|
21 |
+
data = await self.redis.hget(key, "data")
|
22 |
+
if data is None:
|
23 |
+
return UserState()
|
24 |
+
state_dict = msgpack.unpackb(data, raw=False)
|
25 |
+
return UserState.parse_obj(state_dict)
|
26 |
+
|
27 |
+
async def set(self, user_id: str, state: UserState) -> None:
|
28 |
+
"""Persist updated user state."""
|
29 |
+
key = f"llmgamehub:{user_id}"
|
30 |
+
packed = msgpack.packb(json.loads(state.json()))
|
31 |
+
await self.redis.hset(key, mapping={"data": packed})
|
32 |
+
|
33 |
+
async def reset(self, user_id: str) -> None:
|
34 |
+
"""Remove stored state for a user."""
|
35 |
+
key = f"llmgamehub:{user_id}"
|
36 |
+
await self.redis.delete(key)
|
37 |
+
|
38 |
+
|
39 |
+
_repo = UserRepository()
|
40 |
+
|
41 |
+
|
42 |
+
async def get_user_state(user_hash: str) -> UserState:
|
43 |
+
return await _repo.get(user_hash)
|
44 |
+
|
45 |
+
|
46 |
+
async def set_user_state(user_hash: str, state: UserState) -> None:
|
47 |
+
await _repo.set(user_hash, state)
|
48 |
+
|
49 |
+
|
50 |
+
async def reset_user_state(user_hash: str) -> None:
|
51 |
+
await _repo.reset(user_hash)
|
src/agent/runner.py
CHANGED
@@ -10,7 +10,7 @@ from agent.tools import generate_scene_image
|
|
10 |
|
11 |
from agent.llm_graph import GraphState, llm_game_graph
|
12 |
from agent.models import UserState
|
13 |
-
from agent.
|
14 |
|
15 |
logger = logging.getLogger(__name__)
|
16 |
|
|
|
10 |
|
11 |
from agent.llm_graph import GraphState, llm_game_graph
|
12 |
from agent.models import UserState
|
13 |
+
from agent.redis_state import get_user_state
|
14 |
|
15 |
logger = logging.getLogger(__name__)
|
16 |
|
src/agent/state.py
DELETED
@@ -1,24 +0,0 @@
|
|
1 |
-
"""Simple in-memory user state storage."""
|
2 |
-
|
3 |
-
from typing import Dict
|
4 |
-
|
5 |
-
from agent.models import UserState
|
6 |
-
|
7 |
-
_USER_STATE: Dict[str, UserState] = {}
|
8 |
-
|
9 |
-
|
10 |
-
def get_user_state(user_hash: str) -> UserState:
|
11 |
-
"""Return user state for the given id, creating it if necessary."""
|
12 |
-
if user_hash not in _USER_STATE:
|
13 |
-
_USER_STATE[user_hash] = UserState()
|
14 |
-
return _USER_STATE[user_hash]
|
15 |
-
|
16 |
-
|
17 |
-
def set_user_state(user_hash: str, state: UserState) -> None:
|
18 |
-
"""Persist updated user state."""
|
19 |
-
_USER_STATE[user_hash] = state
|
20 |
-
|
21 |
-
|
22 |
-
def reset_user_state(user_hash: str) -> None:
|
23 |
-
"""Reset stored state for a user."""
|
24 |
-
_USER_STATE[user_hash] = UserState()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/agent/tools.py
CHANGED
@@ -17,7 +17,7 @@ from agent.models import (
|
|
17 |
UserChoice,
|
18 |
)
|
19 |
from agent.prompts import ENDING_CHECK_PROMPT, SCENE_PROMPT, STORY_FRAME_PROMPT
|
20 |
-
from agent.
|
21 |
from images.image_generator import modify_image, generate_image
|
22 |
from agent.image_agent import ChangeScene
|
23 |
|
|
|
17 |
UserChoice,
|
18 |
)
|
19 |
from agent.prompts import ENDING_CHECK_PROMPT, SCENE_PROMPT, STORY_FRAME_PROMPT
|
20 |
+
from agent.redis_state import get_user_state, set_user_state
|
21 |
from images.image_generator import modify_image, generate_image
|
22 |
from agent.image_agent import ChangeScene
|
23 |
|
src/app.py
ADDED
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import subprocess
|
2 |
+
import time
|
3 |
+
import atexit
|
4 |
+
import shutil
|
5 |
+
from redis import Redis, ConnectionError
|
6 |
+
|
7 |
+
|
8 |
+
REDIS_BIN = shutil.which("redis-server")
|
9 |
+
|
10 |
+
if not REDIS_BIN:
|
11 |
+
raise RuntimeError("redis-server not found. Ensure redis is installed via packages.txt")
|
12 |
+
|
13 |
+
|
14 |
+
redis_cmd = [
|
15 |
+
REDIS_BIN,
|
16 |
+
"--save",
|
17 |
+
"",
|
18 |
+
"--appendonly",
|
19 |
+
"no",
|
20 |
+
"--dir",
|
21 |
+
"/tmp",
|
22 |
+
"--pidfile",
|
23 |
+
"/tmp/redis.pid",
|
24 |
+
]
|
25 |
+
redis_process = subprocess.Popen(redis_cmd)
|
26 |
+
|
27 |
+
|
28 |
+
redis_client = Redis()
|
29 |
+
for _ in range(20):
|
30 |
+
try:
|
31 |
+
redis_client.ping()
|
32 |
+
break
|
33 |
+
except ConnectionError:
|
34 |
+
time.sleep(0.5)
|
35 |
+
else:
|
36 |
+
raise RuntimeError("Failed to start redis-server")
|
37 |
+
|
38 |
+
|
39 |
+
atexit.register(redis_process.terminate)
|
40 |
+
|
41 |
+
|
42 |
+
time.sleep(0.5)
|
43 |
+
|
44 |
+
import main
|
src/main.py
CHANGED
@@ -21,14 +21,15 @@ import asyncio
|
|
21 |
from game_setting import get_user_story
|
22 |
from config import settings
|
23 |
|
|
|
24 |
logger = logging.getLogger(__name__)
|
25 |
|
26 |
|
27 |
async def return_to_constructor(user_hash: str):
|
28 |
"""Return to the constructor and reset user state and audio."""
|
29 |
-
from agent.
|
30 |
|
31 |
-
reset_user_state(user_hash)
|
32 |
await cleanup_music_session(user_hash)
|
33 |
# Generate a new hash to avoid stale state
|
34 |
new_hash = str(uuid.uuid4())
|
|
|
21 |
from game_setting import get_user_story
|
22 |
from config import settings
|
23 |
|
24 |
+
|
25 |
logger = logging.getLogger(__name__)
|
26 |
|
27 |
|
28 |
async def return_to_constructor(user_hash: str):
|
29 |
"""Return to the constructor and reset user state and audio."""
|
30 |
+
from agent.redis_state import reset_user_state
|
31 |
|
32 |
+
await reset_user_state(user_hash)
|
33 |
await cleanup_music_session(user_hash)
|
34 |
# Generate a new hash to avoid stale state
|
35 |
new_hash = str(uuid.uuid4())
|