Spaces:
Running
Running
Auto commit at 25-2025-08 3:12:15
Browse files- .dockerignore +4 -0
- .gitignore +1 -0
- data/context/room_776_kdy.json +118 -0
- data/context/room_777_kdy.json +166 -0
- data/memory/memory.db +0 -0
- lily_llm_api/api/routers/document_router.py +6 -1
- lily_llm_api/api/routers/generation_router.py +90 -8
- lily_llm_api/api/routers/multimodal_rag_router.py +8 -1
- lily_llm_api/api/routers/user_memory_router.py +58 -0
- lily_llm_api/app.py +10 -5
- lily_llm_api/core/app_factory.py +10 -5
- lily_llm_api/lily_llm_api (2).zip +3 -0
- lily_llm_api/lily_llm_api (3).zip +3 -0
- lily_llm_api/lily_llm_api (4).zip +3 -0
- lily_llm_api/lily_llm_api (5).zip +3 -0
- lily_llm_api/lily_llm_api (6).zip +3 -0
- lily_llm_api/lily_llm_api.zip +3 -0
- lily_llm_api/models/kanana_1_5_v_3b_instruct.py +30 -11
- lily_llm_api/services/generation_service.py +101 -33
- lily_llm_api/services/model_service.py +27 -1
- lily_llm_api/services/session_registry.py +66 -1
- lily_llm_core/lily_llm_core (2).zip +3 -0
- lily_llm_core/lily_llm_core (3).zip +3 -0
- lily_llm_core/lily_llm_core (4).zip +3 -0
- lily_llm_core/lily_llm_core (5).zip +3 -0
- lily_llm_core/lily_llm_core (6).zip +3 -0
- lily_llm_core/lily_llm_core.zip +3 -0
- lily_llm_core/lora_manager.py +25 -2
- lily_llm_core/memory_store.py +179 -0
- requirements.txt +2 -0
.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 |
-
#
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
| 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(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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=
|
| 237 |
-
interval_turns=
|
| 238 |
-
interval_time=
|
| 239 |
-
strategy=
|
| 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=
|
| 54 |
-
interval_turns=
|
| 55 |
-
interval_time=
|
| 56 |
-
strategy=
|
| 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 |
-
|
| 139 |
if device == 'cuda':
|
|
|
|
| 140 |
selected_dtype = torch.bfloat16
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
else:
|
| 142 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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":
|
| 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
|
|
|
|
| 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
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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 |
-
|
|
|
|
| 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=
|
| 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=
|
| 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=
|
| 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=
|
| 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=
|
| 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=
|
| 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 |
-
|
| 952 |
-
|
| 953 |
-
|
| 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 |
-
|
| 963 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 1002 |
-
|
| 1003 |
-
|
|
|
|
| 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=
|
| 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]
|