gbrabbit commited on
Commit
0e9a45c
·
1 Parent(s): 1d1372e

Auto commit at 25-2025-08 3:12:15

Browse files
.dockerignore CHANGED
@@ -17,6 +17,7 @@ uploads/
17
  simple_stores/
18
  notebooks/
19
  lily_llm_etc/
 
20
  *.safetensors
21
  *.pth
22
 
@@ -90,6 +91,9 @@ data/
90
  uploads/
91
  vector_stores/
92
 
 
 
 
93
  # Backup files
94
  backup/
95
  *.backup
 
17
  simple_stores/
18
  notebooks/
19
  lily_llm_etc/
20
+ lily_generate_project/lily_generate_package/data/
21
  *.safetensors
22
  *.pth
23
 
 
91
  uploads/
92
  vector_stores/
93
 
94
+ # Ensure local runtime data under this package is excluded
95
+ ./lily_generate_project/lily_generate_package/data/
96
+
97
  # Backup files
98
  backup/
99
  *.backup
.gitignore CHANGED
@@ -18,6 +18,7 @@ uploads/
18
  simple_stores/
19
  notebooks/
20
  lily_llm_etc/
 
21
  *.safetensors
22
  *.pth
23
 
 
18
  simple_stores/
19
  notebooks/
20
  lily_llm_etc/
21
+ lily_generate_project/lily_generate_package/data/
22
  *.safetensors
23
  *.pth
24
 
data/context/room_776_kdy.json ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "export_timestamp": 1756046222.1794097,
3
+ "session_id": "room_776_kdy",
4
+ "system_prompt": "",
5
+ "conversation_history": [
6
+ {
7
+ "role": "user",
8
+ "content": "안녕 내 이름은 이철수야. 직업은 코딩강사.",
9
+ "timestamp": 1756045958.543232,
10
+ "message_id": "user_1756045958543",
11
+ "metadata": {
12
+ "session_id": "room_776_kdy"
13
+ },
14
+ "summary": "안녕 내 이름은 이철수야. 직업은 코딩강사.",
15
+ "tokens_estimated": 10
16
+ },
17
+ {
18
+ "role": "user",
19
+ "content": "안녕 내 이름은 이철수야. 직업은 코딩강사.",
20
+ "timestamp": 1756046008.1830719,
21
+ "message_id": "user_1756046008183",
22
+ "metadata": {
23
+ "session_id": "room_776_kdy",
24
+ "room_id": "776",
25
+ "images_used": false,
26
+ "num_images": 0
27
+ },
28
+ "summary": "안녕 내 이름은 이철수야. 직업은 코딩강사.",
29
+ "tokens_estimated": 10
30
+ },
31
+ {
32
+ "role": "assistant",
33
+ "content": "안녕하세요, 이철수 강사님! 코딩 강사로서의 경험이 정말 대단하시겠어요. 어떤 코딩 강의를 주로 가르치시나요? 초보자부터 전문가까지 다양한 수준의 학생들을 가르치시나요?",
34
+ "timestamp": 1756046008.1830719,
35
+ "message_id": "assistant_1756046008183",
36
+ "metadata": {
37
+ "session_id": "room_776_kdy",
38
+ "room_id": "776"
39
+ },
40
+ "summary": "안녕하세요, 이철수 강사님! 코딩 강사로서의 경험이 정말 대단하시겠어요. 어떤 코딩 강의를 주로 가르치시나요? 초보자부터 전문가까지 다양한 수준의 학생들을 가르치시나요?",
41
+ "tokens_estimated": 42
42
+ },
43
+ {
44
+ "role": "assistant",
45
+ "content": "안녕하세요, 이철수 강사님! 코딩 강사로서의 경험이 정말 대단하시겠어요. 어떤 코딩 강의를 주로 가르치시나요? 초보자부터 전문가까지 다양한 수준의 학생들을 가르치시나요?",
46
+ "timestamp": 1756046008.1993172,
47
+ "message_id": "assistant_1756046008199",
48
+ "metadata": {
49
+ "session_id": "room_776_kdy"
50
+ },
51
+ "summary": "안녕하세요, 이철수 강사님! 코딩 강사로서의 경험이 정말 대단하시겠어요. 어떤 코딩 강의를 주로 가르치시나요? 초보자부터 전문가까지 다양한 수준의 학생들을 가르치시나요?",
52
+ "tokens_estimated": 42
53
+ },
54
+ {
55
+ "role": "user",
56
+ "content": "블록코딩과 ai 에 대해 가르쳐.",
57
+ "timestamp": 1756046057.3309016,
58
+ "message_id": "user_1756046057330",
59
+ "metadata": {
60
+ "session_id": "room_776_kdy"
61
+ },
62
+ "summary": "블록코딩과 ai 에 대해 가르쳐.",
63
+ "tokens_estimated": 6
64
+ },
65
+ {
66
+ "role": "user",
67
+ "content": "블록코딩과 ai 에 대해 가르쳐.",
68
+ "timestamp": 1756046222.1559293,
69
+ "message_id": "user_1756046222155",
70
+ "metadata": {
71
+ "session_id": "room_776_kdy",
72
+ "room_id": "776",
73
+ "images_used": false,
74
+ "num_images": 0
75
+ },
76
+ "summary": "블록코딩과 ai 에 대해 가르쳐.",
77
+ "tokens_estimated": 6
78
+ },
79
+ {
80
+ "role": "assistant",
81
+ "content": "아, 블록코딩과 AI에 대해 가르치신다니 정말 흥미롭네요! 블록코딩은 초보자들이 코딩의 기본 개념을 쉽게 이해할 수 있도록 돕는 도구로, 스크래치나 마인크래프트",
82
+ "timestamp": 1756046222.1559293,
83
+ "message_id": "assistant_1756046222155",
84
+ "metadata": {
85
+ "session_id": "room_776_kdy",
86
+ "room_id": "776"
87
+ },
88
+ "summary": "아, 블록코딩과 AI에 대해 가르치신다니 정말 흥미롭네요! 블록코딩은 초보자들이 코딩의 기본 개념을 쉽게 이해할 수 있도록 돕는 도구로, 스크래치나 마인크래프트",
89
+ "tokens_estimated": 39
90
+ },
91
+ {
92
+ "role": "assistant",
93
+ "content": "아, 블록코딩과 AI에 대해 가르치신다니 정말 흥미롭네요! 블록코딩은 초보자들이 코딩의 기본 개념을 쉽게 이해할 수 있도록 돕는 도구로, 스크래치나 마인크래프트",
94
+ "timestamp": 1756046222.1730103,
95
+ "message_id": "assistant_1756046222173",
96
+ "metadata": {
97
+ "session_id": "room_776_kdy"
98
+ },
99
+ "summary": "아, 블록코딩과 AI에 대해 가르치신다니 정말 흥미롭네요! 블록코딩은 초보자들이 코딩의 기본 개념을 쉽게 이해할 수 있도록 돕는 도구로, 스크래치나 마인크래프트",
100
+ "tokens_estimated": 39
101
+ }
102
+ ],
103
+ "context_stats": {
104
+ "session_id": "room_776_kdy",
105
+ "total_turns": 8,
106
+ "user_messages": 4,
107
+ "assistant_messages": 4,
108
+ "estimated_tokens": 179,
109
+ "context_length": 716,
110
+ "memory_usage": 0.4,
111
+ "oldest_message": 1756045958.543232,
112
+ "newest_message": 1756046222.1730103,
113
+ "turn_summaries_count": 1,
114
+ "turn_summaries_tokens": 45,
115
+ "compression_count": 0,
116
+ "last_compression": null
117
+ }
118
+ }
data/context/room_777_kdy.json ADDED
@@ -0,0 +1,166 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "export_timestamp": 1756054304.3968902,
3
+ "session_id": "room_777_kdy",
4
+ "system_prompt": "",
5
+ "conversation_history": [
6
+ {
7
+ "role": "user",
8
+ "content": "안녕 내 이름은 김철수야. 직업은 코딩강사.",
9
+ "timestamp": 1756049364.0584369,
10
+ "message_id": "user_1756049364058",
11
+ "metadata": {
12
+ "session_id": "room_777_kdy"
13
+ },
14
+ "summary": "안녕 내 이름은 김철수야. 직업은 코딩강사.",
15
+ "tokens_estimated": 10
16
+ },
17
+ {
18
+ "role": "user",
19
+ "content": "안녕 내 이름은 김철수야. 직업은 코딩강사.",
20
+ "timestamp": 1756049921.6687996,
21
+ "message_id": "user_1756049921668",
22
+ "metadata": {
23
+ "session_id": "room_777_kdy",
24
+ "room_id": "777",
25
+ "images_used": false,
26
+ "num_images": 0
27
+ },
28
+ "summary": "안녕 내 이름은 김철수야. 직업은 코딩강사.",
29
+ "tokens_estimated": 10
30
+ },
31
+ {
32
+ "role": "assistant",
33
+ "content": "안녕하세요, 김철수님! 코딩 강사로서의 경험이 정말 멋지네요. 어떤 종류의 코딩을 가르치시나요? 초보자부터 전문가까지 다양한 수준의 학생들을 가르치시나요? 또는 특정 프로그래밍 언어에",
34
+ "timestamp": 1756049921.6813264,
35
+ "message_id": "assistant_1756049921681",
36
+ "metadata": {
37
+ "session_id": "room_777_kdy",
38
+ "room_id": "777"
39
+ },
40
+ "summary": "안녕하세요, 김철수님! 코딩 강사로서의 경험이 정말 멋지네요. 어떤 종류의 코딩을 가르치시나요? 초보자부터 전문가까지 다양한 수준의 학생들을 가르치시나요? 또는 특정 프로그래밍 언어에",
41
+ "tokens_estimated": 46
42
+ },
43
+ {
44
+ "role": "assistant",
45
+ "content": "안녕하세요, 김철수님! 코딩 강사로서의 경험이 정말 멋지네요. 어떤 종류의 코딩을 가르치시나요? 초보자부터 전문가까지 다양한 수준의 학생들을 가르치시나요? 또는 특정 프로그래밍 언어에",
46
+ "timestamp": 1756049921.6970212,
47
+ "message_id": "assistant_1756049921697",
48
+ "metadata": {
49
+ "session_id": "room_777_kdy"
50
+ },
51
+ "summary": "안녕하세요, 김철수님! 코딩 강사로서의 경험이 정말 멋지네요. 어떤 종류의 코딩을 가르치시나요? 초보자부터 전문가까지 다양한 수준의 학생들을 가르치시나요? 또는 특정 프로그래밍 언어에",
52
+ "tokens_estimated": 46
53
+ },
54
+ {
55
+ "role": "user",
56
+ "content": "블록 코딩을 가르쳐.",
57
+ "timestamp": 1756050060.2033277,
58
+ "message_id": "user_1756050060203",
59
+ "metadata": {
60
+ "session_id": "room_777_kdy"
61
+ },
62
+ "summary": "블록 코딩을 가르쳐.",
63
+ "tokens_estimated": 5
64
+ },
65
+ {
66
+ "role": "user",
67
+ "content": "블록 코딩을 가르쳐.",
68
+ "timestamp": 1756050677.3530622,
69
+ "message_id": "user_1756050677348",
70
+ "metadata": {
71
+ "session_id": "room_777_kdy",
72
+ "room_id": "777",
73
+ "images_used": false,
74
+ "num_images": 0
75
+ },
76
+ "summary": "블록 코딩을 가르쳐.",
77
+ "tokens_estimated": 5
78
+ },
79
+ {
80
+ "role": "assistant",
81
+ "content": "블록 코딩은 초보자들에게 매우 유용한 학습 도구입니다. 어떤 블록 코딩 도구를 사용하고 계신가요? 예를 들어, Scratch, Blockly, 또는 다른 도구가 있을 수 있습니다. 또한, 블록 코딩을 가르칠 때 어떤 방식",
82
+ "timestamp": 1756050677.369919,
83
+ "message_id": "assistant_1756050677369",
84
+ "metadata": {
85
+ "session_id": "room_777_kdy",
86
+ "room_id": "777"
87
+ },
88
+ "summary": "블록 코딩은 초보자들에게 매우 유용한 학습 도구입니다. 어떤 블록 코딩 도구를 사용하고 계신가요? 예를 들어, Scratch, Blockly, 또는 다른 도구가 있을 수 있습니다. 또한, 블록 코딩을 가르칠 때 어떤 방식",
89
+ "tokens_estimated": 51
90
+ },
91
+ {
92
+ "role": "assistant",
93
+ "content": "블록 코딩은 초보자들에게 매우 유용한 학습 도구입니다. 어떤 블록 코딩 도구를 사용하고 계신가요? 예를 들어, Scratch, Blockly, 또는 다른 도구가 있을 수 있습니다. 또한, 블록 코딩을 가르칠 때 어떤 방식",
94
+ "timestamp": 1756050677.4095056,
95
+ "message_id": "assistant_1756050677409",
96
+ "metadata": {
97
+ "session_id": "room_777_kdy"
98
+ },
99
+ "summary": "블록 코딩은 초보자들에게 매우 유용한 학습 도구입니다. 어떤 블록 코딩 도구를 사용하고 계신가요? 예를 들어, Scratch, Blockly, 또는 다른 도구가 있을 수 있습니다. 또한, 블록 코딩을 가르칠 때 어떤 방식",
100
+ "tokens_estimated": 51
101
+ },
102
+ {
103
+ "role": "user",
104
+ "content": "스크래치를 가르쳐. 근데 주로 엠블럭을 사용해. 아두이노와 연동한 프로젝트가 반응이 좋거든",
105
+ "timestamp": 1756054249.7711434,
106
+ "message_id": "user_1756054249771",
107
+ "metadata": {
108
+ "session_id": "room_777_kdy"
109
+ },
110
+ "summary": "스크래치를 가르쳐. 근데 주로 엠블럭을 사용해. 아두이노와 연동한 프로젝트가 반응이 좋거든",
111
+ "tokens_estimated": 23
112
+ },
113
+ {
114
+ "role": "user",
115
+ "content": "스크래치를 가르쳐. 근데 주로 엠블럭을 사용해. 아두이노와 연동한 프로젝트가 반응이 좋거든",
116
+ "timestamp": 1756054304.3831217,
117
+ "message_id": "user_1756054304381",
118
+ "metadata": {
119
+ "session_id": "room_777_kdy",
120
+ "room_id": "777",
121
+ "images_used": false,
122
+ "num_images": 0
123
+ },
124
+ "summary": "스크래치를 가르쳐. 근데 주로 엠블럭을 사용해. 아두이노와 연동한 프로젝트가 반응이 좋거든",
125
+ "tokens_estimated": 23
126
+ },
127
+ {
128
+ "role": "assistant",
129
+ "content": "아두이노와 연동한 프로젝트는 학생들에게 매우 흥미로운 주제가 될 수 있습니다. 엠블럭을 사용하여 아두이노와 연동하는 프로젝트를 가르칠 때, 학생들이 쉽게 따라올 수 있도록 단계별로 설명하는 것이 중요",
130
+ "timestamp": 1756054304.3851252,
131
+ "message_id": "assistant_1756054304385",
132
+ "metadata": {
133
+ "session_id": "room_777_kdy",
134
+ "room_id": "777"
135
+ },
136
+ "summary": "아두이노와 연동한 프로젝트는 학생들에게 매우 흥미로운 주제가 될 수 있습니다. 엠블럭을 사용하여 아두이노와 연동하는 프로젝트를 가르칠 때, 학생들이 쉽게 따라올 수 있도록 단계별로 설명하는 것이 중요",
137
+ "tokens_estimated": 51
138
+ },
139
+ {
140
+ "role": "assistant",
141
+ "content": "아두이노와 연동한 프로젝트는 학생들에게 매우 흥미로운 주제가 될 수 있습니다. 엠블럭을 사용하여 아두이노와 연동하는 프로젝트를 가르칠 때, 학생들이 쉽게 따라올 수 있도록 단계별로 설명하는 것이 중요",
142
+ "timestamp": 1756054304.3928819,
143
+ "message_id": "assistant_1756054304392",
144
+ "metadata": {
145
+ "session_id": "room_777_kdy"
146
+ },
147
+ "summary": "아두이노와 연동한 프로젝트는 학생들에게 매우 흥미로운 주제가 될 수 있습니다. 엠블럭을 사용하여 아두이노와 연동하는 프로젝트를 가르칠 때, 학생들이 쉽게 따라올 수 있도록 단계별로 설명하는 것이 중요",
148
+ "tokens_estimated": 51
149
+ }
150
+ ],
151
+ "context_stats": {
152
+ "session_id": "room_777_kdy",
153
+ "total_turns": 12,
154
+ "user_messages": 6,
155
+ "assistant_messages": 6,
156
+ "estimated_tokens": 307,
157
+ "context_length": 1230,
158
+ "memory_usage": 0.6,
159
+ "oldest_message": 1756049364.0584369,
160
+ "newest_message": 1756054304.3928819,
161
+ "turn_summaries_count": 1,
162
+ "turn_summaries_tokens": 74,
163
+ "compression_count": 0,
164
+ "last_compression": null
165
+ }
166
+ }
data/memory/memory.db ADDED
Binary file (20.5 kB). View file
 
lily_llm_api/api/routers/document_router.py CHANGED
@@ -12,7 +12,7 @@ from ...models.schemas import (
12
  DocumentUploadResponse, RAGQueryRequest, RAGQueryResponse,
13
  DocumentProcessResponse, MultimodalRAGResponse
14
  )
15
- from ...services.session_registry import set_user_for_room
16
 
17
  logger = logging.getLogger(__name__)
18
  router = APIRouter()
@@ -54,6 +54,11 @@ async def upload_document(
54
 
55
  if result.get("success"):
56
  processing_time = time.time() - start_time
 
 
 
 
 
57
  return DocumentUploadResponse(
58
  success=True,
59
  document_id=result.get("document_id", document_id),
 
12
  DocumentUploadResponse, RAGQueryRequest, RAGQueryResponse,
13
  DocumentProcessResponse, MultimodalRAGResponse
14
  )
15
+ from ...services.session_registry import set_user_for_room, set_flag_for_room
16
 
17
  logger = logging.getLogger(__name__)
18
  router = APIRouter()
 
54
 
55
  if result.get("success"):
56
  processing_time = time.time() - start_time
57
+ # 업로드 직후, 같은 방에서 다음 1회 생성은 이미지 복구를 허용
58
+ try:
59
+ set_flag_for_room(room_id, "use_rag_images_once", True)
60
+ except Exception:
61
+ pass
62
  return DocumentUploadResponse(
63
  success=True,
64
  document_id=result.get("document_id", document_id),
lily_llm_api/api/routers/generation_router.py CHANGED
@@ -2,6 +2,7 @@
2
  Generation router for Lily LLM API
3
  """
4
  from fastapi import APIRouter, HTTPException, Request, UploadFile, File, Form, Depends
 
5
  from typing import Optional, List
6
  import logging
7
  import time
@@ -10,7 +11,7 @@ from ...models.schemas import GenerateResponse, MultimodalGenerateResponse
10
  from ...services.generation_service import generate_sync
11
  from ...services.model_service import is_model_loaded
12
  from ...utils.system_utils import select_model_interactive
13
- from ...services.session_registry import get_user_for_room, set_user_for_room, set_user_for_session
14
 
15
  logger = logging.getLogger(__name__)
16
  router = APIRouter()
@@ -29,7 +30,10 @@ async def generate(request: Request,
29
  use_rag_images: bool = Form(False),
30
  use_rag_text: bool = Form(False),
31
  document_id: str = Form(None),
32
- image_short_side: int = Form(None)):
 
 
 
33
 
34
  if not is_model_loaded():
35
  raise HTTPException(status_code=503, detail="모델이 로드되지 않았습니다.")
@@ -46,11 +50,12 @@ async def generate(request: Request,
46
  except Exception:
47
  pass
48
 
49
- # 세션 ID가 없으면 자동 생성 (채팅방별 고유 세션)
50
- if not session_id:
51
- timestamp = int(time.time())
52
- session_id = f"room_{room_id}_user_{user_id}_{timestamp}"
53
- print(f"🔍 [DEBUG] 자동 세션 ID 생성: {session_id} (채팅방: {room_id}, 사용자: {user_id})")
 
54
  else:
55
  # 제공된 세션에도 사용자 매핑 저장
56
  try:
@@ -61,6 +66,31 @@ async def generate(request: Request,
61
  if use_context:
62
  try:
63
  from lily_llm_core.context_manager import context_manager
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  context_manager.add_user_message(prompt, metadata={"session_id": session_id})
65
  print(f"🔍 [DEBUG] 사용자 메시지 추가됨 (세션: {session_id})")
66
  except Exception as e:
@@ -77,8 +107,38 @@ async def generate(request: Request,
77
  logger.warning(f"이미지 로드 실패: {e}")
78
 
79
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  # generate_sync 함수 호출 (컨텍스트 포함)
81
- result = generate_sync(prompt, image_data_list, use_context=use_context, session_id=session_id, user_id=user_id, room_id=room_id, use_rag_images=use_rag_images, use_rag_text=use_rag_text, document_id=document_id, image_short_side=image_short_side)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
  if "error" in result:
84
  raise HTTPException(status_code=500, detail=result["error"])
@@ -87,6 +147,28 @@ async def generate(request: Request,
87
  try:
88
  from lily_llm_core.context_manager import context_manager
89
  context_manager.add_assistant_message(result["generated_text"], metadata={"session_id": session_id})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  except Exception as e:
91
  logger.warning(f"⚠️ 컨텍스트 관리자 사용 불가: {e}")
92
 
 
2
  Generation router for Lily LLM API
3
  """
4
  from fastapi import APIRouter, HTTPException, Request, UploadFile, File, Form, Depends
5
+ import os
6
  from typing import Optional, List
7
  import logging
8
  import time
 
11
  from ...services.generation_service import generate_sync
12
  from ...services.model_service import is_model_loaded
13
  from ...utils.system_utils import select_model_interactive
14
+ from ...services.session_registry import get_user_for_room, set_user_for_room, set_user_for_session, pop_flag_for_room
15
 
16
  logger = logging.getLogger(__name__)
17
  router = APIRouter()
 
30
  use_rag_images: bool = Form(False),
31
  use_rag_text: bool = Form(False),
32
  document_id: str = Form(None),
33
+ image_short_side: int = Form(None),
34
+ # 새 옵션: 생성 토큰 수/입력 길이 제어
35
+ max_new_tokens: int = Form(None),
36
+ input_max_length: int = Form(None)):
37
 
38
  if not is_model_loaded():
39
  raise HTTPException(status_code=503, detail="모델이 로드되지 않았습니다.")
 
50
  except Exception:
51
  pass
52
 
53
+ # 방/사용자 기반 고정 세션 ID로 정규화
54
+ stable_session_id = f"room_{room_id}_{user_id}"
55
+ if not session_id or session_id != stable_session_id:
56
+ original = session_id
57
+ session_id = stable_session_id
58
+ print(f"🔍 [DEBUG] 세션 ID 정규화: {original} -> {session_id}")
59
  else:
60
  # 제공된 세션에도 사용자 매핑 저장
61
  try:
 
66
  if use_context:
67
  try:
68
  from lily_llm_core.context_manager import context_manager
69
+ # 중기/장기 메모리 주입: room/user 기반
70
+ try:
71
+ from lily_llm_core.memory_store import memory_store
72
+ rm = memory_store.get_room_memory(room_id)
73
+ um = memory_store.get_user_memory(user_id)
74
+ note_parts = []
75
+ if rm and (rm.get("summary") or rm.get("key_topics")):
76
+ note_parts.append(f"[Room Memory]\n{rm.get('summary','')}\nTopics: {rm.get('key_topics','')}")
77
+ if um and um.get("notes"):
78
+ note_parts.append(f"[User Memory]\n{um.get('notes','')}")
79
+ if note_parts:
80
+ context_manager.set_system_note(session_id, "hier_mem", "\n\n".join(note_parts))
81
+ except Exception as _me:
82
+ print(f"⚠️ [DEBUG] 메모리 주입 실패: {_me}")
83
+ # 컨텍스트 영속화 설정: 필요 시 세션 파일에서 로드
84
+ try:
85
+ persist = os.getenv('LILY_CONTEXT_PERSIST', '0') in ['1', 'true', 'True']
86
+ if persist:
87
+ base_dir = os.getenv('LILY_CONTEXT_DIR', 'data/context')
88
+ os.makedirs(base_dir, exist_ok=True)
89
+ session_file = os.path.join(base_dir, f"{session_id}.json")
90
+ if hasattr(context_manager, 'load_session_from_file'):
91
+ context_manager.load_session_from_file(session_id, session_file)
92
+ except Exception as _e:
93
+ print(f"⚠️ [DEBUG] 컨텍스트 로드 실패: {_e}")
94
  context_manager.add_user_message(prompt, metadata={"session_id": session_id})
95
  print(f"🔍 [DEBUG] 사용자 메시지 추가됨 (세션: {session_id})")
96
  except Exception as e:
 
107
  logger.warning(f"이미지 로드 실패: {e}")
108
 
109
  try:
110
+ # UX: 첨부 이미지가 있으면 자동으로 멀티모달 허용
111
+ if image_data_list and len([img for img in image_data_list if img]) > 0:
112
+ use_rag_images = True
113
+ # 이미지가 있으면 텍스트 컨텍스트는 유지하고, RAG 텍스트는 기본 그대로 사용
114
+
115
+ # 텍스트-only 강제: 첨부 이미지가 전혀 없고 플래그가 False면 이미지 리스트를 비움
116
+ if (not image_data_list or len([img for img in image_data_list if img]) == 0) and not use_rag_images:
117
+ # 업로드 직후 1회 한정 자동 이미지 허용 플래그가 있으면 소비(pop)하여 True로 전환
118
+ try:
119
+ once_flag = pop_flag_for_room(room_id, "use_rag_images_once")
120
+ if once_flag:
121
+ use_rag_images = True
122
+ print(f"🔍 [DEBUG] 룸 {room_id}에 저장된 1회성 이미지 복구 플래그 사용 -> use_rag_images=True")
123
+ except Exception:
124
+ pass
125
+ image_data_list = []
126
  # generate_sync 함수 호출 (컨텍스트 포함)
127
+ result = generate_sync(
128
+ prompt,
129
+ image_data_list,
130
+ # 생성 길이: 신/구 파라미터 모두 지원 (신>구 우선)
131
+ max_length=max_new_tokens,
132
+ use_context=use_context,
133
+ session_id=session_id,
134
+ user_id=user_id,
135
+ room_id=room_id,
136
+ use_rag_images=use_rag_images,
137
+ use_rag_text=use_rag_text,
138
+ document_id=document_id,
139
+ image_short_side=image_short_side,
140
+ input_max_length=input_max_length
141
+ )
142
 
143
  if "error" in result:
144
  raise HTTPException(status_code=500, detail=result["error"])
 
147
  try:
148
  from lily_llm_core.context_manager import context_manager
149
  context_manager.add_assistant_message(result["generated_text"], metadata={"session_id": session_id})
150
+ # 컨텍스트 영속화: 턴 저장 후 파일로 저장
151
+ try:
152
+ persist = os.getenv('LILY_CONTEXT_PERSIST', '0') in ['1', 'true', 'True']
153
+ if persist:
154
+ base_dir = os.getenv('LILY_CONTEXT_DIR', 'data/context')
155
+ os.makedirs(base_dir, exist_ok=True)
156
+ session_file = os.path.join(base_dir, f"{session_id}.json")
157
+ if hasattr(context_manager, 'save_session_to_file'):
158
+ context_manager.save_session_to_file(session_id, session_file)
159
+ # 중기(room) 메모리 갱신: 최근 턴 요약을 압축 저장
160
+ try:
161
+ from lily_llm_core.memory_store import memory_store
162
+ # 간단히 최근 요약 컨텍스트를 저장하고 키토픽은 비움(후속 확장 지점)
163
+ summary_text = ""
164
+ if hasattr(context_manager, 'get_summary_context'):
165
+ summary_text = context_manager.get_summary_context(session_id)
166
+ last_ts = time.time()
167
+ memory_store.upsert_room_memory(room_id, summary_text, "", last_ts)
168
+ except Exception as _ms:
169
+ print(f"⚠️ [DEBUG] 룸 메모리 저장 실패: {_ms}")
170
+ except Exception as _e:
171
+ print(f"⚠️ [DEBUG] 컨텍스트 저장 실패: {_e}")
172
  except Exception as e:
173
  logger.warning(f"⚠️ 컨텍스트 관리자 사용 불가: {e}")
174
 
lily_llm_api/api/routers/multimodal_rag_router.py CHANGED
@@ -82,7 +82,11 @@ async def generate_hybrid_rag_response(
82
  use_image: bool = Form(True),
83
  use_latex: bool = Form(True),
84
  use_latex_ocr: bool = Form(False), # LaTeX-OCR 기능이 비활성화됨
 
85
  max_length: Optional[int] = Form(None),
 
 
 
86
  temperature: Optional[float] = Form(None),
87
  top_p: Optional[float] = Form(None),
88
  do_sample: Optional[bool] = Form(None)
@@ -91,10 +95,13 @@ async def generate_hybrid_rag_response(
91
  try:
92
  try:
93
  from lily_llm_core.hybrid_rag_processor import hybrid_rag_processor
 
 
94
  result = hybrid_rag_processor.generate_hybrid_response(
95
  query, user_id, document_id,
96
  use_text, use_image, use_latex, use_latex_ocr,
97
- max_length, temperature, top_p, do_sample
 
98
  )
99
  except ImportError:
100
  result = {
 
82
  use_image: bool = Form(True),
83
  use_latex: bool = Form(True),
84
  use_latex_ocr: bool = Form(False), # LaTeX-OCR 기능이 비활성화됨
85
+ # 생성 길이: 기존 max_length에서 새로운 max_new_tokens로 이행
86
  max_length: Optional[int] = Form(None),
87
+ max_new_tokens: Optional[int] = Form(None),
88
+ # 입력 토큰 상한(토크나이즈 최대 길이)
89
+ input_max_length: Optional[int] = Form(None),
90
  temperature: Optional[float] = Form(None),
91
  top_p: Optional[float] = Form(None),
92
  do_sample: Optional[bool] = Form(None)
 
95
  try:
96
  try:
97
  from lily_llm_core.hybrid_rag_processor import hybrid_rag_processor
98
+ # 신/구 파라미터 정리: 신 > 구 우선 적용
99
+ effective_max_new = max_new_tokens if max_new_tokens is not None else max_length
100
  result = hybrid_rag_processor.generate_hybrid_response(
101
  query, user_id, document_id,
102
  use_text, use_image, use_latex, use_latex_ocr,
103
+ effective_max_new, temperature, top_p, do_sample,
104
+ input_max_length=input_max_length
105
  )
106
  except ImportError:
107
  result = {
lily_llm_api/api/routers/user_memory_router.py CHANGED
@@ -7,6 +7,64 @@ import time
7
 
8
  logger = logging.getLogger(__name__)
9
  router = APIRouter()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
  # ============================================================================
12
  # 사용자 메모리 설정 관리 API
 
7
 
8
  logger = logging.getLogger(__name__)
9
  router = APIRouter()
10
+ # ---------------------------------------------------------------------------
11
+ # 1) 사용자 장기 메모 쓰기 경로 (프로필/노트 업데이트)
12
+ # ---------------------------------------------------------------------------
13
+ @router.post("/user/memory/notes/{user_id}")
14
+ async def upsert_user_long_memory(
15
+ user_id: str,
16
+ notes: str = Form("")
17
+ ):
18
+ """사용자 장기 메모(노트) 업데이트 (DB: memory_store, 파일: user_memory_manager 동시 업데이트)"""
19
+ try:
20
+ updated = False
21
+ try:
22
+ from lily_llm_core.memory_store import memory_store
23
+ memory_store.upsert_user_memory(user_id, notes)
24
+ updated = True
25
+ except Exception as _e:
26
+ pass
27
+ try:
28
+ from lily_llm_core.user_memory_manager import user_memory_manager
29
+ user_memory_manager.update_user_memory(user_id, {"important_info": [], "preferences": {}, "name": None})
30
+ # 노트를 별도 키에 반영하고 싶다면 preferences 등에 병합 가능
31
+ except Exception as _e:
32
+ pass
33
+ if updated:
34
+ return {"status": "success", "message": "사용자 장기 메모 업데이트 완료"}
35
+ return {"status": "error", "message": "업데이트에 실패했습니다"}
36
+ except Exception as e:
37
+ return {"status": "error", "message": str(e)}
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # 2) Room 요약 향상: 키토픽 추출 및 압축 요약 저장
41
+ # ---------------------------------------------------------------------------
42
+ @router.post("/room/memory/summary/{room_id}")
43
+ async def upsert_room_summary(
44
+ room_id: str,
45
+ summary: str = Form(""),
46
+ key_topics: str = Form("")
47
+ ):
48
+ """방 요약/키토픽 저장 (DB)"""
49
+ try:
50
+ from lily_llm_core.memory_store import memory_store
51
+ import time
52
+ # summary가 비어있으면 현재 세션 컨텍스트 요약을 사용 가능
53
+ if not summary:
54
+ try:
55
+ from lily_llm_core.context_manager import context_manager
56
+ session_id = f"room_{room_id}_group" # 기본 세션 키 가정
57
+ summary = context_manager.get_summary_context(session_id)
58
+ topics = context_manager.get_key_topics(session_id)
59
+ if topics and not key_topics:
60
+ key_topics = ",".join(topics)
61
+ except Exception:
62
+ pass
63
+ memory_store.upsert_room_memory(room_id, summary, key_topics, time.time())
64
+ return {"status": "success", "message": "Room 요약 저장 완료"}
65
+ except Exception as e:
66
+ return {"status": "error", "message": str(e)}
67
+
68
 
69
  # ============================================================================
70
  # 사용자 메모리 설정 관리 API
lily_llm_api/app.py CHANGED
@@ -231,12 +231,17 @@ async def lifespan(app: FastAPI):
231
  context_manager.set_summary_method("smart")
232
  logger.info("✅ 고급 컨텍스트 관리자 설정 완료: smart 요약 방법 활성화")
233
 
234
- # 자동 정리 설정 최적화
 
 
 
 
 
235
  context_manager.set_auto_cleanup_config(
236
- enabled=True,
237
- interval_turns=5, # 5턴마다 정리
238
- interval_time=180, # 3분마다 정리
239
- strategy="aggressive" # 적극적 정리로 메모리 최적화
240
  )
241
  logger.info("✅ 자동 정리 설정 최적화 완료")
242
 
 
231
  context_manager.set_summary_method("smart")
232
  logger.info("✅ 고급 컨텍스트 관리자 설정 완료: smart 요약 방법 활성화")
233
 
234
+ # 자동 정리 설정 (환경변수로 오버라이드)
235
+ import os
236
+ enabled = os.getenv('LILY_CONTEXT_AUTOCLEAN_ENABLED', '1') in ['1', 'true', 'True']
237
+ interval_turns = int(os.getenv('LILY_CONTEXT_AUTOCLEAN_TURNS', '12'))
238
+ interval_time = int(os.getenv('LILY_CONTEXT_AUTOCLEAN_TIME', '600'))
239
+ strategy = os.getenv('LILY_CONTEXT_CLEANUP_STRATEGY', 'smart')
240
  context_manager.set_auto_cleanup_config(
241
+ enabled=enabled,
242
+ interval_turns=interval_turns,
243
+ interval_time=interval_time,
244
+ strategy=strategy
245
  )
246
  logger.info("✅ 자동 정리 설정 최적화 완료")
247
 
lily_llm_api/core/app_factory.py CHANGED
@@ -48,12 +48,17 @@ async def create_lifespan_handler(app):
48
  context_manager.set_summary_method("smart")
49
  logger.info("✅ 고급 컨텍스트 관리자 설정 완료: smart 요약 방법 활성화")
50
 
51
- # 자동 정리 설정 최적화
 
 
 
 
 
52
  context_manager.set_auto_cleanup_config(
53
- enabled=True,
54
- interval_turns=5, # 5턴마다 정리
55
- interval_time=180, # 3분마다 정리
56
- strategy="aggressive" # 적극적 정리로 메모리 최적화
57
  )
58
  logger.info("✅ 자동 정리 설정 최적화 완료")
59
 
 
48
  context_manager.set_summary_method("smart")
49
  logger.info("✅ 고급 컨텍스트 관리자 설정 완료: smart 요약 방법 활성화")
50
 
51
+ # 자동 정리 설정 (환경변수로 오버라이드)
52
+ import os
53
+ enabled = os.getenv('LILY_CONTEXT_AUTOCLEAN_ENABLED', '1') in ['1', 'true', 'True']
54
+ interval_turns = int(os.getenv('LILY_CONTEXT_AUTOCLEAN_TURNS', '12'))
55
+ interval_time = int(os.getenv('LILY_CONTEXT_AUTOCLEAN_TIME', '600'))
56
+ strategy = os.getenv('LILY_CONTEXT_CLEANUP_STRATEGY', 'smart')
57
  context_manager.set_auto_cleanup_config(
58
+ enabled=enabled,
59
+ interval_turns=interval_turns,
60
+ interval_time=interval_time,
61
+ strategy=strategy
62
  )
63
  logger.info("✅ 자동 정리 설정 최적화 완료")
64
 
lily_llm_api/lily_llm_api (2).zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7b6fb58fe03727aade71d05ee53c4f9a20d7dbba7edf4109467b43f05d577304
3
+ size 1141004
lily_llm_api/lily_llm_api (3).zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7b6fb58fe03727aade71d05ee53c4f9a20d7dbba7edf4109467b43f05d577304
3
+ size 1141004
lily_llm_api/lily_llm_api (4).zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0ae06631193791cd4e6ca17c13043776014d7a3407ca0aa326defd17f2e819c1
3
+ size 1144984
lily_llm_api/lily_llm_api (5).zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:36963d2dcdf0aa657731f7d2824add788c1f1eae5ba057d89cdd8e8e95c2e2c8
3
+ size 1154904
lily_llm_api/lily_llm_api (6).zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:062f623c164562954d2f8ac9631416eed4b7d2b8e31a916a4dfe6bd234d1ec94
3
+ size 1153684
lily_llm_api/lily_llm_api.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f9585cfd3668c8377d7b1a1971084b479212eabc1e1657b6bd8aa4513247a756
3
+ size 1136689
lily_llm_api/models/kanana_1_5_v_3b_instruct.py CHANGED
@@ -134,12 +134,30 @@ class Kanana15V3bInstructProfile:
134
  )
135
 
136
  device = 'cuda' if torch.cuda.is_available() else 'cpu'
137
-
138
- # 공식 설정 파일 bfloat16 사용, float32 사용시 메모리 에러 발생
139
  if device == 'cuda':
 
140
  selected_dtype = torch.bfloat16
 
 
 
 
 
 
 
 
141
  else:
142
- selected_dtype = torch.bfloat16
 
 
 
 
 
 
 
 
 
143
 
144
  logger.info(f"🔧 선택된 dtype: {selected_dtype} (device: {device})")
145
 
@@ -167,8 +185,16 @@ class Kanana15V3bInstructProfile:
167
 
168
 
169
  def get_generation_config(self) -> Dict[str, Any]:
 
 
 
 
 
 
 
 
170
  return {
171
- "max_new_tokens": 128,
172
  "do_sample": True,
173
  "temperature": 0.7,
174
  "top_k": 50,
@@ -178,13 +204,6 @@ class Kanana15V3bInstructProfile:
178
  "pad_token_id": 128001,
179
  "eos_token_id": 128009,
180
  "bos_token_id": 128000,
181
- # "use_cache": False,
182
- # "early_stopping": False,
183
- # "num_beams": 1,
184
- # "num_return_sequences": 1,
185
- # "return_full_text": False,
186
- # "return_dict": False,
187
- # "return_dict_in_generate": False,
188
  }
189
 
190
  def extract_response(self, full_text: str, formatted_prompt: str = None, **kwargs) -> str:
 
134
  )
135
 
136
  device = 'cuda' if torch.cuda.is_available() else 'cpu'
137
+ # 환경변수로 dtype 제어 (기본: CPU=float32, CUDA=bfloat16)
138
+ env_dtype = (os.getenv('LILY_FORCE_DTYPE') or os.getenv('LILY_CPU_DTYPE') if device=='cpu' else os.getenv('LILY_CUDA_DTYPE'))
139
  if device == 'cuda':
140
+ env_dtype = os.getenv('LILY_FORCE_DTYPE') or os.getenv('LILY_CUDA_DTYPE')
141
  selected_dtype = torch.bfloat16
142
+ if env_dtype:
143
+ m = env_dtype.lower()
144
+ if m in ('float16','fp16'):
145
+ selected_dtype = torch.float16
146
+ elif m in ('float32','fp32'):
147
+ selected_dtype = torch.float32
148
+ elif m in ('bfloat16','bf16'):
149
+ selected_dtype = torch.bfloat16
150
  else:
151
+ # CPU 기본은 float32 (속도/호환성)
152
+ selected_dtype = torch.float32
153
+ if env_dtype:
154
+ m = env_dtype.lower()
155
+ if m in ('float16','fp16'):
156
+ selected_dtype = torch.float16
157
+ elif m in ('float32','fp32'):
158
+ selected_dtype = torch.float32
159
+ elif m in ('bfloat16','bf16'):
160
+ selected_dtype = torch.bfloat16
161
 
162
  logger.info(f"🔧 선택된 dtype: {selected_dtype} (device: {device})")
163
 
 
185
 
186
 
187
  def get_generation_config(self) -> Dict[str, Any]:
188
+ import os
189
+ def _get_int(env_key: str, default_val: int) -> int:
190
+ try:
191
+ v = int(os.getenv(env_key, str(default_val)))
192
+ return v if v > 0 else default_val
193
+ except Exception:
194
+ return default_val
195
+ max_new = _get_int('LILY_MAX_NEW_TOKENS', 128)
196
  return {
197
+ "max_new_tokens": max_new,
198
  "do_sample": True,
199
  "temperature": 0.7,
200
  "top_k": 50,
 
204
  "pad_token_id": 128001,
205
  "eos_token_id": 128009,
206
  "bos_token_id": 128000,
 
 
 
 
 
 
 
207
  }
208
 
209
  def extract_response(self, full_text: str, formatted_prompt: str = None, **kwargs) -> str:
lily_llm_api/services/generation_service.py CHANGED
@@ -34,7 +34,8 @@ def generate_sync(prompt: str, image_data_list: Optional[List[bytes]], max_lengt
34
  do_sample: Optional[bool] = None, use_context: bool = True, session_id: str = None,
35
  user_id: str = "anonymous", room_id: str = "default", use_rag_images: bool = False,
36
  use_rag_text: bool = False, document_id: Optional[str] = None,
37
- image_short_side: Optional[int] = None) -> dict:
 
38
  """[최적화] 모델 생성을 처리하는 통합 동기 함수"""
39
  try:
40
  from .model_service import get_current_profile, get_current_model
@@ -78,11 +79,40 @@ def generate_sync(prompt: str, image_data_list: Optional[List[bytes]], max_lengt
78
 
79
  print(f"🔍 [DEBUG] 모델 이름: {getattr(current_profile, 'model_name', 'Unknown')}")
80
  print(f"🔍 [DEBUG] 멀티모달 지원: {getattr(current_profile, 'multimodal', False)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
  print(f"🔍 [DEBUG] 입력 프롬프트: {prompt}")
82
  print(f"🔍 [DEBUG] 입력 프롬프트 길이: {len(prompt)}")
83
  print(f"🔍 [DEBUG] 이미지 데이터 존재 여부: {image_data_list is not None}")
84
  print(f"🔍 [DEBUG] 이미지 데이터 개수: {len(image_data_list) if image_data_list else 0}")
85
  print(f"🔍 [DEBUG] 실제 이미지 데이터 개수: {len([img for img in image_data_list if img]) if image_data_list else 0}")
 
86
 
87
  image_processed = False
88
  all_pixel_values = []
@@ -94,11 +124,14 @@ def generate_sync(prompt: str, image_data_list: Optional[List[bytes]], max_lengt
94
  all_image_data.extend(image_data_list)
95
  print(f"🔍 [DEBUG] 직접 전달된 이미지 {len(image_data_list)}개 추가")
96
  else:
97
- # 현재 요청에 이미지가 없으면 세션 캐시에서 복구 시도
98
- if session_id and session_id in _session_image_cache and len(_session_image_cache[session_id]) > 0:
99
- cached_imgs = _session_image_cache[session_id]
100
- all_image_data.extend(cached_imgs)
101
- print(f"🔍 [DEBUG] 세션 캐시에서 이전 이미지 {len(cached_imgs)}개 복구 (세션: {session_id})")
 
 
 
102
 
103
  # 추가 복구: 여전히 이미지가 없고 멀티모달이며, 명시적으로 허용된 경우에만 RAG에서 이미지 복원
104
  if use_rag_images and (not all_image_data or len([img for img in all_image_data if img]) == 0) and getattr(current_profile, 'multimodal', False):
@@ -138,7 +171,8 @@ def generate_sync(prompt: str, image_data_list: Optional[List[bytes]], max_lengt
138
  except Exception as e:
139
  print(f"⚠️ [DEBUG] RAG 기반 이미지 복구 실패: {e}")
140
  elif not use_rag_images and getattr(current_profile, 'multimodal', False):
141
- print("🔍 [DEBUG] RAG 이미지 복구 비활성화됨(use_rag_images=False) - 텍스트 전용 유지")
 
142
 
143
  # 항상 참조 가능한 max_images 정의 (이미지 없으면 0)
144
  # 1차 상한은 4장으로 제한 (최종 선택은 예산 기반 동적 선택에서 결정)
@@ -234,6 +268,8 @@ def generate_sync(prompt: str, image_data_list: Optional[List[bytes]], max_lengt
234
 
235
  # 🔧 이미지 토큰 예산 기반 동적 선택 (멀티모달 길이 초과 방지)
236
  try:
 
 
237
  # 1) 이미지별 토큰 수 산출
238
  per_image_tokens: List[int] = []
239
  if isinstance(combined_image_metas, dict) and 'image_token_thw' in combined_image_metas:
@@ -247,7 +283,8 @@ def generate_sync(prompt: str, image_data_list: Optional[List[bytes]], max_lengt
247
  per_image_tokens = [3000] * len(all_pixel_values)
248
 
249
  # 2) 텍스트 길이 측정 (이미지 토큰 제외한 프롬프트)
250
- base_text_prompt = f"Human: {prompt}\nAssistant:"
 
251
  text_inputs = tokenizer(
252
  base_text_prompt,
253
  return_tensors="pt",
@@ -375,8 +412,9 @@ def generate_sync(prompt: str, image_data_list: Optional[List[bytes]], max_lengt
375
  num_images = len(all_pixel_values)
376
  image_tokens = "<image>" * num_images # 이미지 개수만큼 <image> 토큰 생성
377
  # 답변 유도를 위해 Assistant 프리픽스 추가
378
- # 길이 초과를 방지하기 위해 멀티모달 경로에서는 사용자 입력만 포함
379
- formatted_prompt = f"Human: {image_tokens}{prompt}\nAssistant:"
 
380
  print(f"🔍 [DEBUG] 멀티모달 프롬프트 구성 (공식 형식): {formatted_prompt}")
381
  print(f"🔍 [DEBUG] 이미지 토큰 생성: {num_images}개 이미지 -> {image_tokens}")
382
  image_processed = True
@@ -412,7 +450,8 @@ def generate_sync(prompt: str, image_data_list: Optional[List[bytes]], max_lengt
412
  print(f"🔍 [DEBUG] 기본 프롬프트 사용 (컨텍스트 포함): {formatted_prompt}")
413
 
414
  print(f"🔍 [DEBUG] 프롬프트 구성 완료 - 길이: {len(formatted_prompt) if formatted_prompt else 0}")
415
- print(f"🔍 [DEBUG] 최종 프롬프트: {formatted_prompt}")
 
416
 
417
  # --- 3. 토크나이징 ---
418
  print(f"🔍 [DEBUG] 토크나이징 시작")
@@ -423,12 +462,18 @@ def generate_sync(prompt: str, image_data_list: Optional[List[bytes]], max_lengt
423
  print(f"🔍 [DEBUG] 텍스트-only 토크나이징 경로")
424
  print(f"🔍 [DEBUG] 사용할 프롬프트: {formatted_prompt}")
425
 
 
 
 
 
 
 
426
  inputs = tokenizer(
427
  formatted_prompt,
428
  return_tensors="pt",
429
  padding=True,
430
  truncation=True,
431
- max_length=2048,
432
  )
433
  if 'token_type_ids' in inputs:
434
  del inputs['token_type_ids']
@@ -514,7 +559,7 @@ def generate_sync(prompt: str, image_data_list: Optional[List[bytes]], max_lengt
514
  # 🔄 공식 방식: max_length 파라미터 추가
515
  inputs = tokenizer.encode_prompt(
516
  prompt=formatted_prompt,
517
- max_length=2048, # 공식 코드와 동일
518
  image_meta=final_meta
519
  )
520
  print(f"🔍 [DEBUG] encode_prompt 출력: {list(inputs.keys())}")
@@ -560,7 +605,7 @@ def generate_sync(prompt: str, image_data_list: Optional[List[bytes]], max_lengt
560
  print(f"🔁 [DEBUG] 재시도 limit={limit}: {base_prompt_retry}")
561
  inputs_retry = tokenizer.encode_prompt(
562
  prompt=base_prompt_retry,
563
- max_length=2048,
564
  image_meta=final_meta
565
  )
566
  # 정규화
@@ -585,7 +630,7 @@ def generate_sync(prompt: str, image_data_list: Optional[List[bytes]], max_lengt
585
  print(f"🔁 [DEBUG] encode_prompt 재시도(컨텍스트 제거): {base_prompt_retry}")
586
  inputs = tokenizer.encode_prompt(
587
  prompt=base_prompt_retry,
588
- max_length=2048,
589
  image_meta=final_meta
590
  )
591
  print(f"🔍 [DEBUG] encode_prompt 재시도 성공: {list(inputs.keys())}")
@@ -599,7 +644,7 @@ def generate_sync(prompt: str, image_data_list: Optional[List[bytes]], max_lengt
599
  return_tensors="pt",
600
  padding=True,
601
  truncation=True,
602
- max_length=2048,
603
  )
604
  if 'token_type_ids' in inputs:
605
  del inputs['token_type_ids']
@@ -613,7 +658,7 @@ def generate_sync(prompt: str, image_data_list: Optional[List[bytes]], max_lengt
613
  return_tensors="pt",
614
  padding=True,
615
  truncation=True,
616
- max_length=2048,
617
  )
618
  if 'token_type_ids' in inputs:
619
  del inputs['token_type_ids']
@@ -675,6 +720,20 @@ def generate_sync(prompt: str, image_data_list: Optional[List[bytes]], max_lengt
675
  if do_sample is not None:
676
  gen_config['do_sample'] = do_sample
677
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
678
  print(f"🔍 [DEBUG] 생성 설정: {gen_config}")
679
 
680
  # --- 5. 실제 추론 실행 ---
@@ -948,31 +1007,39 @@ def generate_sync(prompt: str, image_data_list: Optional[List[bytes]], max_lengt
948
  'attention_mask': attention_mask
949
  }
950
 
951
- generated_ids = lora_model.generate(
952
- **lora_inputs,
953
- **gen_config
954
- )
 
 
955
  else:
956
  print(f"⚠️ [DEBUG] LoRA 모델을 가져올 수 없음, 기본 모델 사용")
 
 
 
 
 
 
 
 
 
 
 
957
  generated_ids = current_model.generate(
958
  input_ids=input_ids,
959
  attention_mask=attention_mask,
960
  **gen_config
961
  )
962
- else:
963
- print(f"🔍 [DEBUG] LoRA 어댑터 없음, 기본 모델 사용")
 
 
964
  generated_ids = current_model.generate(
965
  input_ids=input_ids,
966
  attention_mask=attention_mask,
967
  **gen_config
968
  )
969
- except ImportError:
970
- print(f"🔍 [DEBUG] LoRA 지원 안됨, 기본 모델 사용")
971
- generated_ids = current_model.generate(
972
- input_ids=input_ids,
973
- attention_mask=attention_mask,
974
- **gen_config
975
- )
976
 
977
  print(f"🔍 [DEBUG] 모델 생성 완료 시간: {time.time()}")
978
 
@@ -998,9 +1065,10 @@ def generate_sync(prompt: str, image_data_list: Optional[List[bytes]], max_lengt
998
  try:
999
  # 생성된 텍스트 디코딩
1000
  full_text = tokenizer.decode(generated_ids[0], skip_special_tokens=True)
1001
- print(f"🔍 [DEBUG] 전체 텍스트 길이: {len(full_text)}")
1002
- print(f"🔍 [DEBUG] 전체 생성 텍스트 (Raw): \n---\n{full_text}\n---")
1003
- print(f"🔍 [DEBUG] 사용된 프롬프트: {formatted_prompt}")
 
1004
 
1005
  # 프로필별 응답 추출 (안전한 방식)
1006
  if hasattr(current_profile, 'extract_response'):
 
34
  do_sample: Optional[bool] = None, use_context: bool = True, session_id: str = None,
35
  user_id: str = "anonymous", room_id: str = "default", use_rag_images: bool = False,
36
  use_rag_text: bool = False, document_id: Optional[str] = None,
37
+ image_short_side: Optional[int] = None,
38
+ input_max_length: Optional[int] = None) -> dict:
39
  """[최적화] 모델 생성을 처리하는 통합 동기 함수"""
40
  try:
41
  from .model_service import get_current_profile, get_current_model
 
79
 
80
  print(f"🔍 [DEBUG] 모델 이름: {getattr(current_profile, 'model_name', 'Unknown')}")
81
  print(f"🔍 [DEBUG] 멀티모달 지원: {getattr(current_profile, 'multimodal', False)}")
82
+ # 속도/로그 모드 플래그 (환경변수)
83
+ speed_mode = os.getenv('LILY_SPEED_MODE', '0') == '1'
84
+ debug_log_prompt = os.getenv('LILY_DEBUG_LOG_PROMPT', '0') == '1'
85
+ debug_log_text = os.getenv('LILY_DEBUG_LOG_TEXT', '0') == '1'
86
+ # CPU에서 bf16은 느릴 수 있으므로 필요 시 float32로 전환
87
+ try:
88
+ import torch as _torch
89
+ if hasattr(current_model, 'device') and str(current_model.device) == 'cpu':
90
+ desired = (os.getenv('LILY_FORCE_DTYPE') or os.getenv('LILY_CPU_DTYPE') or 'float32').lower()
91
+ desired_map = {
92
+ 'float32': _torch.float32,
93
+ 'fp32': _torch.float32,
94
+ 'bfloat16': _torch.bfloat16,
95
+ 'bf16': _torch.bfloat16,
96
+ 'float16': _torch.float16,
97
+ 'fp16': _torch.float16,
98
+ }
99
+ target_dtype = desired_map.get(desired, _torch.float32)
100
+ if hasattr(current_model, 'dtype') and current_model.dtype != target_dtype:
101
+ print(f"🔧 [SPEED] CPU dtype 전환: {current_model.dtype} -> {target_dtype}")
102
+ current_model = current_model.to(target_dtype)
103
+ except Exception as _dtype_e:
104
+ print(f"⚠️ [SPEED] dtype 전환 실패: {_dtype_e}")
105
+ # 항상 eval 모드
106
+ try:
107
+ current_model.eval()
108
+ except Exception:
109
+ pass
110
  print(f"🔍 [DEBUG] 입력 프롬프트: {prompt}")
111
  print(f"🔍 [DEBUG] 입력 프롬프트 길이: {len(prompt)}")
112
  print(f"🔍 [DEBUG] 이미지 데이터 존재 여부: {image_data_list is not None}")
113
  print(f"🔍 [DEBUG] 이미지 데이터 개수: {len(image_data_list) if image_data_list else 0}")
114
  print(f"🔍 [DEBUG] 실제 이미지 데이터 개수: {len([img for img in image_data_list if img]) if image_data_list else 0}")
115
+ print(f"🔍 [DEBUG] use_rag_images 플래그: {use_rag_images}")
116
 
117
  image_processed = False
118
  all_pixel_values = []
 
124
  all_image_data.extend(image_data_list)
125
  print(f"🔍 [DEBUG] 직접 전달된 이미지 {len(image_data_list)}개 추가")
126
  else:
127
+ # 현재 요청에 이미지가 없으면 (옵션) 세션 캐시에서 복구
128
+ if use_rag_images:
129
+ if session_id and session_id in _session_image_cache and len(_session_image_cache[session_id]) > 0:
130
+ cached_imgs = _session_image_cache[session_id]
131
+ all_image_data.extend(cached_imgs)
132
+ print(f"🔍 [DEBUG] 세션 캐시에서 이전 이미지 {len(cached_imgs)}개 복구 (세션: {session_id})")
133
+ else:
134
+ print("🔍 [DEBUG] 세션 캐시 복구 비활성화(use_rag_images=False)")
135
 
136
  # 추가 복구: 여전히 이미지가 없고 멀티모달이며, 명시적으로 허용된 경우에만 RAG에서 이미지 복원
137
  if use_rag_images and (not all_image_data or len([img for img in all_image_data if img]) == 0) and getattr(current_profile, 'multimodal', False):
 
171
  except Exception as e:
172
  print(f"⚠️ [DEBUG] RAG 기반 이미지 복구 실패: {e}")
173
  elif not use_rag_images and getattr(current_profile, 'multimodal', False):
174
+ # RAG 기반의 추가 이미지 복구만 비활성화. 직접 첨부 이미지는 유지한다.
175
+ print("🔍 [DEBUG] RAG 이미지 복구 비활성화됨(use_rag_images=False) - 직접 첨부 이미지만 허용")
176
 
177
  # 항상 참조 가능한 max_images 정의 (이미지 없으면 0)
178
  # 1차 상한은 4장으로 제한 (최종 선택은 예산 기반 동적 선택에서 결정)
 
268
 
269
  # 🔧 이미지 토큰 예산 기반 동적 선택 (멀티모달 길이 초과 방지)
270
  try:
271
+ # 예산 계산에서 사용할 RAG 스니펫 기본값 (후단에서 실제 계산됨)
272
+ rag_snippet_short = ""
273
  # 1) 이미지별 토큰 수 산출
274
  per_image_tokens: List[int] = []
275
  if isinstance(combined_image_metas, dict) and 'image_token_thw' in combined_image_metas:
 
283
  per_image_tokens = [3000] * len(all_pixel_values)
284
 
285
  # 2) 텍스트 길이 측정 (이미지 토큰 제외한 프롬프트)
286
+ # 멀티모달에서도 RAG 스니펫(축약)을 포함하여 텍스트 길이를 산정
287
+ base_text_prompt = f"Human: {rag_snippet_short}{prompt}\nAssistant:"
288
  text_inputs = tokenizer(
289
  base_text_prompt,
290
  return_tensors="pt",
 
412
  num_images = len(all_pixel_values)
413
  image_tokens = "<image>" * num_images # 이미지 개수만큼 <image> 토큰 생성
414
  # 답변 유도를 위해 Assistant 프리픽스 추가
415
+ # 멀티모달에서도 RAG 텍스트 스니펫(축약)을 앞에 포함하여 텍스트 근거를 반영
416
+ mm_text = f"{rag_snippet_short}{prompt}" if rag_snippet_short else prompt
417
+ formatted_prompt = f"Human: {image_tokens}{mm_text}\nAssistant:"
418
  print(f"🔍 [DEBUG] 멀티모달 프롬프트 구성 (공식 형식): {formatted_prompt}")
419
  print(f"🔍 [DEBUG] 이미지 토큰 생성: {num_images}개 이미지 -> {image_tokens}")
420
  image_processed = True
 
450
  print(f"🔍 [DEBUG] 기본 프롬프트 사용 (컨텍스트 포함): {formatted_prompt}")
451
 
452
  print(f"🔍 [DEBUG] 프롬프트 구성 완료 - 길이: {len(formatted_prompt) if formatted_prompt else 0}")
453
+ if debug_log_prompt:
454
+ print(f"🔍 [DEBUG] 최종 프롬프트: {formatted_prompt}")
455
 
456
  # --- 3. 토크나이징 ---
457
  print(f"🔍 [DEBUG] 토크나이징 시작")
 
462
  print(f"🔍 [DEBUG] 텍스트-only 토크나이징 경로")
463
  print(f"🔍 [DEBUG] 사용할 프롬프트: {formatted_prompt}")
464
 
465
+ # 입력 최대 길이: 사용자 입력 > 환경변수 > 기본값
466
+ try:
467
+ env_input_max_len = int(os.getenv('LILY_INPUT_MAX_LENGTH', '0'))
468
+ except Exception:
469
+ env_input_max_len = 0
470
+ effective_input_max_len = input_max_length or (env_input_max_len if env_input_max_len > 0 else (1024 if speed_mode else 2048))
471
  inputs = tokenizer(
472
  formatted_prompt,
473
  return_tensors="pt",
474
  padding=True,
475
  truncation=True,
476
+ max_length=effective_input_max_len,
477
  )
478
  if 'token_type_ids' in inputs:
479
  del inputs['token_type_ids']
 
559
  # 🔄 공식 방식: max_length 파라미터 추가
560
  inputs = tokenizer.encode_prompt(
561
  prompt=formatted_prompt,
562
+ max_length=effective_input_max_len,
563
  image_meta=final_meta
564
  )
565
  print(f"🔍 [DEBUG] encode_prompt 출력: {list(inputs.keys())}")
 
605
  print(f"🔁 [DEBUG] 재시도 limit={limit}: {base_prompt_retry}")
606
  inputs_retry = tokenizer.encode_prompt(
607
  prompt=base_prompt_retry,
608
+ max_length=effective_input_max_len,
609
  image_meta=final_meta
610
  )
611
  # 정규화
 
630
  print(f"🔁 [DEBUG] encode_prompt 재시도(컨텍스트 제거): {base_prompt_retry}")
631
  inputs = tokenizer.encode_prompt(
632
  prompt=base_prompt_retry,
633
+ max_length=effective_input_max_len,
634
  image_meta=final_meta
635
  )
636
  print(f"🔍 [DEBUG] encode_prompt 재시도 성공: {list(inputs.keys())}")
 
644
  return_tensors="pt",
645
  padding=True,
646
  truncation=True,
647
+ max_length=effective_input_max_len,
648
  )
649
  if 'token_type_ids' in inputs:
650
  del inputs['token_type_ids']
 
658
  return_tensors="pt",
659
  padding=True,
660
  truncation=True,
661
+ max_length=effective_input_max_len,
662
  )
663
  if 'token_type_ids' in inputs:
664
  del inputs['token_type_ids']
 
720
  if do_sample is not None:
721
  gen_config['do_sample'] = do_sample
722
 
723
+ # 속도 우선 모드일 때 생성 설정 다이어트
724
+ if speed_mode:
725
+ try:
726
+ gen_config['do_sample'] = False
727
+ gen_config['top_k'] = None
728
+ gen_config['top_p'] = 1.0
729
+ gen_config['repetition_penalty'] = 1.0
730
+ gen_config['no_repeat_ngram_size'] = 0
731
+ if 'max_new_tokens' in gen_config and gen_config['max_new_tokens'] is not None:
732
+ gen_config['max_new_tokens'] = min(int(gen_config['max_new_tokens']), 64)
733
+ else:
734
+ gen_config['max_new_tokens'] = 64
735
+ except Exception as _e_cfg:
736
+ print(f"⚠️ [SPEED] 생성 설정 다이어트 실패: {_e_cfg}")
737
  print(f"🔍 [DEBUG] 생성 설정: {gen_config}")
738
 
739
  # --- 5. 실제 추론 실행 ---
 
1007
  'attention_mask': attention_mask
1008
  }
1009
 
1010
+ import torch as _torch
1011
+ with _torch.inference_mode():
1012
+ generated_ids = lora_model.generate(
1013
+ **lora_inputs,
1014
+ **gen_config
1015
+ )
1016
  else:
1017
  print(f"⚠️ [DEBUG] LoRA 모델을 가져올 수 없음, 기본 모델 사용")
1018
+ import torch as _torch
1019
+ with _torch.inference_mode():
1020
+ generated_ids = current_model.generate(
1021
+ input_ids=input_ids,
1022
+ attention_mask=attention_mask,
1023
+ **gen_config
1024
+ )
1025
+ else:
1026
+ print(f"🔍 [DEBUG] LoRA 어댑터 없음, 기본 모델 사용")
1027
+ import torch as _torch
1028
+ with _torch.inference_mode():
1029
  generated_ids = current_model.generate(
1030
  input_ids=input_ids,
1031
  attention_mask=attention_mask,
1032
  **gen_config
1033
  )
1034
+ except ImportError:
1035
+ print(f"🔍 [DEBUG] LoRA 지원 안됨, 기본 모델 사용")
1036
+ import torch as _torch
1037
+ with _torch.inference_mode():
1038
  generated_ids = current_model.generate(
1039
  input_ids=input_ids,
1040
  attention_mask=attention_mask,
1041
  **gen_config
1042
  )
 
 
 
 
 
 
 
1043
 
1044
  print(f"🔍 [DEBUG] 모델 생성 완료 시간: {time.time()}")
1045
 
 
1065
  try:
1066
  # 생성된 텍스트 디코딩
1067
  full_text = tokenizer.decode(generated_ids[0], skip_special_tokens=True)
1068
+ if os.getenv('LILY_DEBUG_LOG_TEXT', '0') == '1':
1069
+ print(f"🔍 [DEBUG] 전체 텍스트 길이: {len(full_text)}")
1070
+ print(f"🔍 [DEBUG] 전체 생성 텍스트 (Raw): \n---\n{full_text}\n---")
1071
+ print(f"🔍 [DEBUG] 사용된 프롬프트: {formatted_prompt}")
1072
 
1073
  # 프로필별 응답 추출 (안전한 방식)
1074
  if hasattr(current_profile, 'extract_response'):
lily_llm_api/services/model_service.py CHANGED
@@ -2,6 +2,7 @@
2
  Model service for Lily LLM API
3
  """
4
  import logging
 
5
  import asyncio
6
  import concurrent.futures
7
  from typing import Optional
@@ -55,7 +56,32 @@ def load_model_sync(model_id: str):
55
 
56
  # 이제 load_model은 (model, processor)를 반환합니다.
57
  model, processor = current_profile.load_model()
58
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  # 🔄 전역 변수에 모델 설정 (LoRA에서 사용)
60
  current_model = model
61
 
 
2
  Model service for Lily LLM API
3
  """
4
  import logging
5
+ import os
6
  import asyncio
7
  import concurrent.futures
8
  from typing import Optional
 
56
 
57
  # 이제 load_model은 (model, processor)를 반환합니다.
58
  model, processor = current_profile.load_model()
59
+
60
+ # 🔧 서버 시작 시점에서 dtype 강제 적용 (첫 요청 지연 방지)
61
+ try:
62
+ import torch as _torch
63
+ # 디바이스별 대상 dtype 결정 (기본: CPU=float32, CUDA=bfloat16)
64
+ if hasattr(model, 'device') and str(model.device) == 'cpu':
65
+ desired = (os.getenv('LILY_FORCE_DTYPE') or os.getenv('LILY_CPU_DTYPE') or 'float32').lower()
66
+ default_target = _torch.float32
67
+ else:
68
+ desired = (os.getenv('LILY_FORCE_DTYPE') or os.getenv('LILY_CUDA_DTYPE') or 'bfloat16').lower()
69
+ default_target = _torch.bfloat16
70
+ desired_map = {
71
+ 'float32': _torch.float32,
72
+ 'fp32': _torch.float32,
73
+ 'bfloat16': _torch.bfloat16,
74
+ 'bf16': _torch.bfloat16,
75
+ 'float16': _torch.float16,
76
+ 'fp16': _torch.float16,
77
+ }
78
+ target_dtype = desired_map.get(desired, default_target)
79
+ if hasattr(model, 'dtype') and model.dtype != target_dtype:
80
+ logger.info(f"🔧 [SPEED][startup] dtype 적용: {model.dtype} -> {target_dtype}")
81
+ model = model.to(target_dtype)
82
+ except Exception as _dtype_e:
83
+ logger.warning(f"⚠️ [startup] dtype 적용 실패: {_dtype_e}")
84
+
85
  # 🔄 전역 변수에 모델 설정 (LoRA에서 사용)
86
  current_model = model
87
 
lily_llm_api/services/session_registry.py CHANGED
@@ -3,13 +3,15 @@
3
  - 프로세스 메모리 기반 (서비스 재시작 시 초기화)
4
  - 업로드/생성 간 user_id 불일치 보정용
5
  """
6
- from typing import Optional, Dict
7
  import time
 
8
 
9
  _room_to_user: Dict[str, str] = {}
10
  _session_to_user: Dict[str, str] = {}
11
  _last_user: Optional[str] = None
12
  _last_updated_at: float = 0.0
 
13
 
14
  def set_user_for_room(room_id: Optional[str], user_id: Optional[str]) -> None:
15
  if not room_id or not user_id:
@@ -50,4 +52,67 @@ def clear() -> None:
50
  _last_user = None
51
  _last_updated_at = 0.0
52
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
 
 
3
  - 프로세스 메모리 기반 (서비스 재시작 시 초기화)
4
  - 업로드/생성 간 user_id 불일치 보정용
5
  """
6
+ from typing import Optional, Dict, Any
7
  import time
8
+ import os
9
 
10
  _room_to_user: Dict[str, str] = {}
11
  _session_to_user: Dict[str, str] = {}
12
  _last_user: Optional[str] = None
13
  _last_updated_at: float = 0.0
14
+ _room_flags: Dict[str, Dict[str, Dict[str, Any]]] = {}
15
 
16
  def set_user_for_room(room_id: Optional[str], user_id: Optional[str]) -> None:
17
  if not room_id or not user_id:
 
52
  _last_user = None
53
  _last_updated_at = 0.0
54
 
55
+ # ---- room-scoped one-shot flags ----
56
+ def set_flag_for_room(room_id: Optional[str], key: str, value: bool = True, ttl_seconds: Optional[float] = None) -> None:
57
+ if not room_id or not key:
58
+ return
59
+ rid = str(room_id)
60
+ flags = _room_flags.get(rid)
61
+ if flags is None:
62
+ flags = {}
63
+ _room_flags[rid] = flags
64
+ # TTL 결정: 인자 > 환경변수 > 기본 120초
65
+ if ttl_seconds is None:
66
+ try:
67
+ env_ttl = os.getenv('LILY_RAG_IMAGE_FLAG_TTL_SECONDS')
68
+ ttl_seconds = float(env_ttl) if env_ttl is not None else 120.0
69
+ except Exception:
70
+ ttl_seconds = 120.0
71
+ expires_at = time.time() + float(ttl_seconds) if ttl_seconds and ttl_seconds > 0 else None
72
+ flags[str(key)] = {"value": bool(value), "expires_at": expires_at}
73
+
74
+ def get_flag_for_room(room_id: Optional[str], key: str) -> Optional[bool]:
75
+ if not room_id or not key:
76
+ return None
77
+ rid = str(room_id)
78
+ flags = _room_flags.get(rid) or {}
79
+ entry = flags.get(str(key))
80
+ if not entry:
81
+ return None
82
+ expires_at = entry.get("expires_at")
83
+ if expires_at is not None and time.time() > expires_at:
84
+ # 만료되면 제거
85
+ try:
86
+ flags.pop(str(key), None)
87
+ except Exception:
88
+ pass
89
+ return None
90
+ return bool(entry.get("value"))
91
+
92
+ def pop_flag_for_room(room_id: Optional[str], key: str) -> Optional[bool]:
93
+ if not room_id or not key:
94
+ return None
95
+ rid = str(room_id)
96
+ flags = _room_flags.get(rid)
97
+ if not flags:
98
+ return None
99
+ entry = flags.get(str(key))
100
+ if not entry:
101
+ return None
102
+ expires_at = entry.get("expires_at")
103
+ if expires_at is not None and time.time() > expires_at:
104
+ # 만료된 경우 소비 없이 제거
105
+ try:
106
+ flags.pop(str(key), None)
107
+ except Exception:
108
+ pass
109
+ return None
110
+ try:
111
+ removed = flags.pop(str(key), None)
112
+ except Exception:
113
+ removed = None
114
+ if removed is None:
115
+ return None
116
+ return bool(removed.get("value"))
117
+
118
 
lily_llm_core/lily_llm_core (2).zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f9ad7c0597cf61ee2dc67f5891da819b1809640835bf11123d8b4a144d48b8fe
3
+ size 1934920
lily_llm_core/lily_llm_core (3).zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:fc71cf6d83b343051d3a96996fb4444ba213219c87d20c49abe82e077234adf5
3
+ size 467736
lily_llm_core/lily_llm_core (4).zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:f3d10427cd12ff84032904e80ff66edfbd3ea1f4541fbcc091546eea12cd5722
3
+ size 995879
lily_llm_core/lily_llm_core (5).zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:46564b960e68c833cd25844a9dd1292c1b42bff30e4926be09e153794fae5fc9
3
+ size 997411
lily_llm_core/lily_llm_core (6).zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7674a856489f61a0c78c8cdf1584c74dcbb3db537d22d8af17062404fa11a37e
3
+ size 990717
lily_llm_core/lily_llm_core.zip ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:47c145852d0b39b15d41e5b9eee35367c7a16d8f070465744ed2bfb3f2de9202
3
+ size 962275
lily_llm_core/lora_manager.py CHANGED
@@ -167,17 +167,40 @@ class LoRAManager:
167
  str(model_path),
168
  trust_remote_code=True,
169
  local_files_only=True,
170
- torch_dtype=torch.float16 if self.device == "cuda" else torch.float32,
171
  device_map="auto" if self.device == "cuda" else None
172
  )
173
  elif model_type == "vision2seq":
174
  # 🔄 Vision2Seq 모델 지원 추가 (kanana 등, bfloat16 사용)
175
  from transformers import AutoModelForVision2Seq
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
176
  self.base_model = AutoModelForVision2Seq.from_pretrained(
177
  str(model_path),
178
  trust_remote_code=True,
179
  local_files_only=True,
180
- torch_dtype=torch.bfloat16 if self.device == "cuda" else torch.bfloat16,
181
  device_map="auto" if self.device == "cuda" else None
182
  )
183
  else:
 
167
  str(model_path),
168
  trust_remote_code=True,
169
  local_files_only=True,
170
+ torch_dtype=(torch.float16 if self.device == "cuda" else torch.float32),
171
  device_map="auto" if self.device == "cuda" else None
172
  )
173
  elif model_type == "vision2seq":
174
  # 🔄 Vision2Seq 모델 지원 추가 (kanana 등, bfloat16 사용)
175
  from transformers import AutoModelForVision2Seq
176
+ # 환경변수로 dtype 제어: CPU 기본 float32
177
+ env_dtype = (os.getenv('LILY_FORCE_DTYPE') or os.getenv('LILY_CPU_DTYPE') if self.device=='cpu' else os.getenv('LILY_CUDA_DTYPE'))
178
+ if self.device == 'cuda':
179
+ env_dtype = os.getenv('LILY_FORCE_DTYPE') or os.getenv('LILY_CUDA_DTYPE')
180
+ selected_dtype = torch.bfloat16
181
+ if env_dtype:
182
+ m = env_dtype.lower()
183
+ if m in ('float16','fp16'):
184
+ selected_dtype = torch.float16
185
+ elif m in ('float32','fp32'):
186
+ selected_dtype = torch.float32
187
+ elif m in ('bfloat16','bf16'):
188
+ selected_dtype = torch.bfloat16
189
+ else:
190
+ selected_dtype = torch.float32
191
+ if env_dtype:
192
+ m = env_dtype.lower()
193
+ if m in ('float16','fp16'):
194
+ selected_dtype = torch.float16
195
+ elif m in ('float32','fp32'):
196
+ selected_dtype = torch.float32
197
+ elif m in ('bfloat16','bf16'):
198
+ selected_dtype = torch.bfloat16
199
  self.base_model = AutoModelForVision2Seq.from_pretrained(
200
  str(model_path),
201
  trust_remote_code=True,
202
  local_files_only=True,
203
+ torch_dtype=selected_dtype,
204
  device_map="auto" if self.device == "cuda" else None
205
  )
206
  else:
lily_llm_core/memory_store.py ADDED
@@ -0,0 +1,179 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Room/User memory store with pluggable backend
4
+ - Preferred: SQLAlchemy with DATABASE URL (MySQL/Postgres/SQLite)
5
+ - Fallback: SQLite (sqlite3) only if SQLAlchemy or URL not available
6
+ """
7
+ import os
8
+ import time
9
+ from typing import Optional, Dict, Any
10
+
11
+ # ---- SQLAlchemy backend (preferred) ----
12
+ SQLA_AVAILABLE = False
13
+ try:
14
+ from sqlalchemy import create_engine, Column, Text, String, Float, LargeBinary
15
+ from sqlalchemy.orm import declarative_base, sessionmaker
16
+ SQLA_AVAILABLE = True
17
+ except Exception:
18
+ SQLA_AVAILABLE = False
19
+
20
+ LILY_MEMORY_URL = os.getenv('LILY_MEMORY_URL')
21
+
22
+ if SQLA_AVAILABLE and LILY_MEMORY_URL:
23
+ Base = declarative_base()
24
+
25
+ class RoomMemory(Base):
26
+ __tablename__ = 'room_memory'
27
+ room_id = Column(String(255), primary_key=True)
28
+ summary = Column(Text)
29
+ key_topics = Column(Text)
30
+ last_turn_ts = Column(Float)
31
+ updated_at = Column(Float)
32
+
33
+ class UserMemory(Base):
34
+ __tablename__ = 'user_memory'
35
+ user_id = Column(String(255), primary_key=True)
36
+ notes = Column(Text)
37
+ embedding = Column(LargeBinary)
38
+ updated_at = Column(Float)
39
+
40
+ _engine = create_engine(LILY_MEMORY_URL, echo=os.getenv('LILY_MEMORY_ECHO', '0') in ['1','true','True'])
41
+ Base.metadata.create_all(_engine)
42
+ _Session = sessionmaker(bind=_engine)
43
+
44
+ class MemoryStore:
45
+ def __init__(self) -> None:
46
+ pass
47
+
48
+ # ---- Room mid-term memory ----
49
+ def upsert_room_memory(self, room_id: str, summary: str, key_topics_csv: str, last_turn_ts: float) -> None:
50
+ now = time.time()
51
+ with _Session() as s:
52
+ obj = s.get(RoomMemory, str(room_id))
53
+ if obj is None:
54
+ obj = RoomMemory(room_id=str(room_id))
55
+ obj.summary = summary or ''
56
+ obj.key_topics = key_topics_csv or ''
57
+ obj.last_turn_ts = float(last_turn_ts or now)
58
+ obj.updated_at = now
59
+ s.merge(obj)
60
+ s.commit()
61
+
62
+ def get_room_memory(self, room_id: str) -> Optional[Dict[str, Any]]:
63
+ with _Session() as s:
64
+ obj = s.get(RoomMemory, str(room_id))
65
+ if not obj:
66
+ return None
67
+ return {"summary": obj.summary or '', "key_topics": obj.key_topics or ''}
68
+
69
+ # ---- User long-term memory ----
70
+ def upsert_user_memory(self, user_id: str, notes: str, embedding: Optional[bytes] = None) -> None:
71
+ now = time.time()
72
+ with _Session() as s:
73
+ obj = s.get(UserMemory, str(user_id))
74
+ if obj is None:
75
+ obj = UserMemory(user_id=str(user_id))
76
+ obj.notes = notes or ''
77
+ obj.embedding = embedding
78
+ obj.updated_at = now
79
+ s.merge(obj)
80
+ s.commit()
81
+
82
+ def get_user_memory(self, user_id: str) -> Optional[Dict[str, Any]]:
83
+ with _Session() as s:
84
+ obj = s.get(UserMemory, str(user_id))
85
+ if not obj:
86
+ return None
87
+ return {"notes": obj.notes or ''}
88
+
89
+ memory_store = MemoryStore()
90
+
91
+ else:
92
+ # ---- SQLite fallback (sqlite3) ----
93
+ import sqlite3
94
+
95
+ DB_PATH = os.getenv('LILY_MEMORY_DB', 'data/memory/memory.db')
96
+
97
+ SCHEMA_SQL = """
98
+ CREATE TABLE IF NOT EXISTS room_memory (
99
+ room_id TEXT PRIMARY KEY,
100
+ summary TEXT,
101
+ key_topics TEXT,
102
+ last_turn_ts REAL,
103
+ updated_at REAL
104
+ );
105
+
106
+ CREATE TABLE IF NOT EXISTS user_memory (
107
+ user_id TEXT PRIMARY KEY,
108
+ notes TEXT,
109
+ embedding BLOB,
110
+ updated_at REAL
111
+ );
112
+ """
113
+
114
+ class MemoryStore:
115
+ def __init__(self, db_path: Optional[str] = None) -> None:
116
+ self.db_path = db_path or DB_PATH
117
+ os.makedirs(os.path.dirname(self.db_path), exist_ok=True)
118
+ self._init_db()
119
+
120
+ def _connect(self):
121
+ return sqlite3.connect(self.db_path)
122
+
123
+ def _init_db(self):
124
+ with self._connect() as conn:
125
+ conn.executescript(SCHEMA_SQL)
126
+ conn.commit()
127
+
128
+ # ---- Room mid-term memory ----
129
+ def upsert_room_memory(self, room_id: str, summary: str, key_topics_csv: str, last_turn_ts: float) -> None:
130
+ now = time.time()
131
+ with self._connect() as conn:
132
+ conn.execute(
133
+ """
134
+ INSERT INTO room_memory(room_id, summary, key_topics, last_turn_ts, updated_at)
135
+ VALUES(?,?,?,?,?)
136
+ ON CONFLICT(room_id) DO UPDATE SET
137
+ summary=excluded.summary,
138
+ key_topics=excluded.key_topics,
139
+ last_turn_ts=excluded.last_turn_ts,
140
+ updated_at=excluded.updated_at
141
+ """,
142
+ (str(room_id), summary or '', key_topics_csv or '', float(last_turn_ts or now), now)
143
+ )
144
+ conn.commit()
145
+
146
+ def get_room_memory(self, room_id: str) -> Optional[Dict[str, Any]]:
147
+ with self._connect() as conn:
148
+ cur = conn.execute("SELECT summary, key_topics FROM room_memory WHERE room_id=?", (str(room_id),))
149
+ row = cur.fetchone()
150
+ if not row:
151
+ return None
152
+ return {"summary": row[0] or '', "key_topics": row[1] or ''}
153
+
154
+ # ---- User long-term memory ----
155
+ def upsert_user_memory(self, user_id: str, notes: str, embedding: Optional[bytes] = None) -> None:
156
+ now = time.time()
157
+ with self._connect() as conn:
158
+ conn.execute(
159
+ """
160
+ INSERT INTO user_memory(user_id, notes, embedding, updated_at)
161
+ VALUES(?,?,?,?)
162
+ ON CONFLICT(user_id) DO UPDATE SET
163
+ notes=excluded.notes,
164
+ embedding=excluded.embedding,
165
+ updated_at=excluded.updated_at
166
+ """,
167
+ (str(user_id), notes or '', embedding, now)
168
+ )
169
+ conn.commit()
170
+
171
+ def get_user_memory(self, user_id: str) -> Optional[Dict[str, Any]]:
172
+ with self._connect() as conn:
173
+ cur = conn.execute("SELECT notes FROM user_memory WHERE user_id=?", (str(user_id),))
174
+ row = cur.fetchone()
175
+ if not row:
176
+ return None
177
+ return {"notes": row[0] or ''}
178
+
179
+ memory_store = MemoryStore()
requirements.txt CHANGED
@@ -43,6 +43,8 @@ python-docx
43
  python-pptx
44
  pytesseract
45
  sqlalchemy
 
 
46
  celery
47
  redis
48
  python-jose[cryptography]
 
43
  python-pptx
44
  pytesseract
45
  sqlalchemy
46
+ psycopg2-binary
47
+ PyMySQL
48
  celery
49
  redis
50
  python-jose[cryptography]