File size: 17,738 Bytes
7f91cdf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
# -*- coding: utf-8 -*-

import sys
import os
import json
import time
import copy
import argparse
from typing import Dict, Any
from openai import OpenAI
import re
import openai

# Add memory bank path
sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'memory_bank'))

# Configuration for OpenAI API
CHATGPT_CONFIG = {
    "model": "gpt-4o",
    "temperature": 0.7,
    "max_tokens": 400,
    "top_p": 1.0,
    "frequency_penalty": 0.4,
    "presence_penalty": 0.2,
    "stop": ["<|im_end|>", "¬Human"]
}

# Default memory directory
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
MEMORY_DIR = os.path.join(BASE_DIR, "memories", "update_memory_0512_eng.json")

class LLMClientSimple:
    def __init__(self, config, gen_config=None):
        self.config = config
        self.client = OpenAI(
            api_key=self.config.get("api_key") or os.environ.get("GAPGPT_API_KEY"),
            base_url=self.config.get("base_url") or os.environ.get(
                "GAPGPT_BASE_URL", "https://api.gapgpt.app/v1"
            ),
        )
        self.disable_tqdm = False
        self.gen_config = copy.deepcopy(
            gen_config if gen_config is not None else config
        )

        self.disable_tqdm = False
        self.gen_config = gen_config

    def generate_text_simple(self, prompt, prompt_num, language='en'):
        self.gen_config['n'] = prompt_num
        retry_times, count = 5, 0
        response = None
        
        while response is None and count < retry_times:
            try:
                request = copy.deepcopy(self.gen_config)
                
                # System prompt setup based on language
                if language == 'cn':
                    message = [
                        {"role": "system", "content": "The following is a transcript of a conversation between a human and a smart, psychology-savvy AI assistant."},
                        {"role": "user", "content": "Hello! Please help me summarize the conversation"},
                        {"role": "system", "content": "OK, I'll try to help you."},
                        {"role": "user", "content": f"{prompt}"}
                    ]
                else:
                    message = [
                        {"role": "system", "content": "Below is a transcript of a conversation between a human and an AI assistant that is intelligent and knowledgeable in psychology."},
                        {"role": "user", "content": "Hello! Please help me summarize the content of the conversation."},
                        {"role": "system", "content": "Sure, I will do my best to assist you."},
                        {"role": "user", "content": f"{prompt}"}
                    ]

                # Call the API method
                response = self.client.chat.completions.create(**request, messages=message)

            except Exception as e:
                print(e)
                # Check for context limit errors
                if 'context' in str(e).lower() or 'maximum' in str(e).lower():
                    cut_length = 1800 - 200 * count
                    print('Max context length reached, cut to {}'.format(cut_length))
                    prompt = prompt[-cut_length:]
                    response = None
                count += 1
        
        if response:
            # Get response content using dot notation
            task_desc = response.choices[0].message.content
        else:
            task_desc = ''
            
        return task_desc


# Lazy initialization instead of module-level
_llm_client = None

def get_llm_client():
    global _llm_client
    if _llm_client is None:
        _llm_client = LLMClientSimple(CHATGPT_CONFIG, gen_config=CHATGPT_CONFIG)
    return _llm_client



# --- Prompt Generation Functions ---

def summarize_content_prompt(content, user_name, boot_name, language='en'):
    # Base prompt adjusted based on psychology
    prompt = 'Please summarize the following dialogue from a psychological perspective, focusing on the user\'s emotional state, key concerns, and potential therapeutic insights. Dialogue content:\n'
    
    for dialog in content:
        query = dialog['query']
        response = dialog['response']
        prompt += f"\n{user_name}: {query.strip()}"
        prompt += f"\n{boot_name}: {response.strip()}"
    
    prompt += ('\nSummary:' if language == 'cn' else '\nSummarization:')
    return prompt


def summarize_user_issues_prompt(content, user_name, boot_name, language='en'):
    """
    Analyze the conversation from a psychological perspective, identify user problems and their causes,
    and organize them into a structured JSON format.
    """
    prompt = (
        "Please analyze the following dialogue from a psychological perspective. Identify the user's key "
        "problems and the underlying causes behind them. Structure the results in a clear and concise way. "
        "Dialogue content:\n"
    )

    for dialog in content:
        query = dialog['query']
        response = dialog['response']
        prompt += f"\n{user_name}: {query.strip()}"
        prompt += f"\n{boot_name}: {response.strip()}"

    prompt += '\n\nPlease list the user’s problems and their causes in JSON format, like this:\n'
    prompt += '{\n  "problems": [\n    {\n      "problem": "<problem_1>",\n      "cause": "<cause_1>"\n    },\n    {\n      "problem": "<problem_2>",\n      "cause": "<cause_2>"\n    }\n  ]\n}'

    return prompt


def summarize_overall_prompt(content, language='en'):
    prompt = 'Please summarize the following events in a high-level manner, and try to be as concise as possible, summarizing and retaining the core key information:\n' if language == 'cn' else "Please provide a highly concise summary of the following event, capturing the essential key information as succinctly as possible. Summarize the event:\n"
    
    for date, summary_dict in content:
        summary = summary_dict['content']
        prompt += (f"\nTime {date} The event that occurred was {summary.strip()}" if language == 'cn' else f"At {date}, the events are {summary.strip()}")
    
    prompt += ('\nSummary:' if language == 'cn' else '\nSummarization:')
    return prompt


def summarize_overall_personality(content, language='en'):
    prompt = 'Below are the personality traits and moods of users in multiple conversations, as well as the appropriate response strategies at the time:\n' if language == 'cn' else "The following are the user's exhibited personality traits and emotions throughout multiple dialogues, along with appropriate response strategies for the current situation:"
    
    for date, summary in content:
        prompt += (f"\n in time {date} The analysis is {summary.strip()}" if language == 'cn' else f"At {date}, the analysis shows {summary.strip()}")
    
    prompt += ('\nPlease give a general summary of the users personality and the most appropriate response strategy for the AI lover, and try to be concise and highly summarized. The summary is:' if language == 'cn' else "Please provide a highly concise and general summary of the user's personality and the most appropriate response strategy for the AI lover, summarized as:")
    return prompt


def summarize_person_prompt(content, user_name, boot_name, language):
    prompt = f"Based on the following dialogue, analyze {user_name}'s emotional state, cognitive patterns, and potential psychological needs. Suggest therapeutic strategies for the AI to respond empathetically and effectively. Dialogue content:\n"
    
    for dialog in content:
        query = dialog['query']
        response = dialog['response']
        prompt += f"\n{user_name}: {query.strip()}"
        prompt += f"\n{boot_name}: {response.strip()}"

    prompt += (f'\n{user_name} personality traits, mood, {boot_name} The response strategy is:' if language == 'cn' else f"\n{user_name}'s personality traits, emotions, and {boot_name}'s response strategy are:")
    return prompt


# --- Memory Extraction Functions ---

def extract_session_summary(conversation_text, session_date, session_id):
    """
    Extracts session-level insights and creates an episodic memory entry.

    Args:
        conversation_text (str): The full conversation from the session.
        session_date (str): The date of the session.
        session_id (int): The index of the session.

    Returns:
        dict: Extracted episodic memory entry.
    """
    # Prompt to the LLM to extract insights
    insight_prompt = f"""
Please analyze the following conversation and extract key information:

Conversation:
{conversation_text}

1. Topics discussed (as a list)
2. User's emotional state (e.g., happy, anxious, stressed)
3. Key insights or takeaways (what the user is feeling or thinking)

Format response as JSON:
{{
  "topics_discussed": [],
  "emotional_state": "",
  "insights": ""
}}
"""

    response = get_llm_client().generate_text_simple(
        prompt=insight_prompt,
        prompt_num=1,
        language="en",
    )

    # Remove extra whitespace
    response = response.strip()

    # حذف تگ شروع بلاک کد مارک‌داون
    if response.startswith("```json"):response = response[7:]
    elif response.startswith("```"):response = response[3:]

    # حذف تگ پایان بلاک کد مارک‌داون
    if response.endswith("```"):
        response = response[:-3]

        response = response.strip()

    try:
        episodic_memory = json.loads(response)
    except json.JSONDecodeError:
        print("⚠️ Failed to parse episodic memory response!")
        episodic_memory = {
        "topics_discussed": ["N/A"],
        "emotional_state": "unknown",
        "insights": "No insights extracted.",
        }

    # Add session metadata
    episodic_memory["session_date"] = session_date
    episodic_memory["session_id"] = session_id

    return episodic_memory



def extract_semantic_memory(latest_episodic_memory, existing_semantic_memory):
    """
    Updates semantic memory based on the latest episodic memory.
    """
    # Use the insights from episodic memory to infer stable traits and patterns
    semantic_prompt = f"""
    Please analyze the following session summary and infer long-term personality traits and stable characteristics:
    Session Summary:
    {json.dumps(latest_episodic_memory, indent=4)}

    Update the user's semantic memory with:
    1. Evolving personality traits (Big Five).
    2. Core values and recurring motivations.
    3. Behavioral patterns and consistent emotional responses.
    4. Frequent topics and recurring themes.

    Format response as JSON:
    {{
    "personality_traits": {{
    "openness": 0.0,
    "conscientiousness": 0.0,
    "extraversion": 0.0,
    "agreeableness": 0.0,
    "neuroticism": 0.0
    }},
    "core_values": [],
    "behavioral_patterns": [],
    "recurring_themes": []
    }}
    """

    response = get_llm_client().generate_text_simple(prompt=semantic_prompt, prompt_num=1, language='en')

    # Safe JSON extraction using Regex
    try:
        # Find content between first { and last }
        json_match = re.search(r'\{.*\}', response, re.DOTALL)
        if json_match:
            json_str = json_match.group(0)
            new_semantic_data = json.loads(json_str)
        else:
            raise ValueError("No JSON object found in response.")

    except Exception as e:
        print(f"⚠️ Failed to parse semantic memory response! Error: {e}")
        print(f"LLM Response was:\n{response}") 
        return existing_semantic_memory

    # Merge new data into semantic memory
    updated_semantic_memory = existing_semantic_memory if isinstance(existing_semantic_memory, dict) else {}

    # Update or average personality traits
    for trait, value in new_semantic_data.get("personality_traits", {}).items():
        try:
            val_float = float(value)
        except (ValueError, TypeError):
            val_float = 0.0

        if trait in updated_semantic_memory.get("personality_traits", {}):
            existing_value = float(updated_semantic_memory["personality_traits"].get(trait, 0.0))
            updated_semantic_memory["personality_traits"][trait] = round((existing_value + val_float) / 2, 2)
        else:
            updated_semantic_memory.setdefault("personality_traits", {})[trait] = val_float

    # Combine core values, avoiding duplicates
    updated_semantic_memory["core_values"] = list(set(
        updated_semantic_memory.get("core_values", []) + new_semantic_data.get("core_values", [])
    ))

    # Combine behavioral patterns
    updated_semantic_memory["behavioral_patterns"] = list(set(
        updated_semantic_memory.get("behavioral_patterns", []) + new_semantic_data.get("behavioral_patterns", [])
    ))

    # Combine recurring themes
    updated_semantic_memory["recurring_themes"] = list(set(
        updated_semantic_memory.get("recurring_themes", []) + new_semantic_data.get("recurring_themes", [])
    ))

    print("\n✅ Semantic memory updated successfully!")
    return updated_semantic_memory

    # Merge new data into semantic memory
    updated_semantic_memory = existing_semantic_memory if isinstance(existing_semantic_memory, dict) else {}

    # Update or average personality traits
    for trait, value in new_semantic_data.get("personality_traits", {}).items():
        try:
            val_float = float(value)
        except (ValueError, TypeError):
            val_float = 0.0

        if trait in updated_semantic_memory.get("personality_traits", {}):
            existing_value = float(updated_semantic_memory["personality_traits"].get(trait, 0.0))
            updated_semantic_memory["personality_traits"][trait] = round((existing_value + val_float) / 2, 2)
        else:
            updated_semantic_memory.setdefault("personality_traits", {})[trait] = val_float

    # Combine core values, avoiding duplicates
    updated_semantic_memory["core_values"] = list(set(
        updated_semantic_memory.get("core_values", []) + new_semantic_data.get("core_values", [])
    ))

    # Combine behavioral patterns
    updated_semantic_memory["behavioral_patterns"] = list(set(
        updated_semantic_memory.get("behavioral_patterns", []) + new_semantic_data.get("behavioral_patterns", [])
    ))

    # Combine recurring themes
    updated_semantic_memory["recurring_themes"] = list(set(
        updated_semantic_memory.get("recurring_themes", []) + new_semantic_data.get("recurring_themes", [])
    ))

    print("\n✅ Semantic memory updated successfully!")
    return updated_semantic_memory


# --- Main Logic ---

def summarize_memory(memory_dir, name=None, language='en'):
    boot_name = 'AI'
    gen_prompt_num = 1

    with open(memory_dir, 'r', encoding='utf8') as f:
        memory = json.loads(f.read())

    for k, v in memory.items():
        if name is not None and k != name:
            continue

        user_name = k
        print(f'Updating memory for user {user_name}')

        if v.get('history') is None:
            continue

        history = v['history']

        if v.get('summary') is None:
            memory[user_name]['summary'] = {}
        if v.get('personality') is None:
            memory[user_name]['personality'] = {}
        if v.get('issues') is None:
            memory[user_name]['issues'] = {}

        for date, content in history.items():
            # Check flags to see if update is needed
            his_flag = False if (date in v['summary'].keys() and v['summary'][date]) else True
            person_flag = False if (date in v['personality'].keys() and v['personality'][date]) else True
            problem_flag = False if (date in v['issues'].keys() and v['issues'][date]) else True 

            hisprompt = summarize_content_prompt(content, user_name, boot_name, language)
            issues_prompt = summarize_user_issues_prompt(content, user_name, boot_name, language)
            person_prompt = summarize_person_prompt(content, user_name, boot_name, language)

            if his_flag:
                his_summary = get_llm_client().generate_text_simple(prompt=hisprompt, prompt_num=gen_prompt_num, language=language)
                memory[user_name]['summary'][date] = {'content': his_summary}

            if problem_flag:
                issues_summary = get_llm_client().generate_text_simple(prompt=issues_prompt, prompt_num=gen_prompt_num, language=language)
                memory[user_name]['issues'][date] = {'content': issues_summary}
                print(issues_summary)

            if person_flag:
                person_summary = get_llm_client().generate_text_simple(prompt=person_prompt, prompt_num=gen_prompt_num, language=language)
                memory[user_name]['personality'][date] = person_summary

        overall_his_prompt = summarize_overall_prompt(list(memory[user_name]['summary'].items()), language=language)
        overall_person_prompt = summarize_overall_personality(list(memory[user_name]['personality'].items()), language=language)

        memory[user_name]['overall_history'] = get_llm_client().generate_text_simple(prompt=overall_his_prompt, prompt_num=gen_prompt_num, language=language)
        memory[user_name]['overall_personality'] = get_llm_client().generate_text_simple(prompt=overall_person_prompt, prompt_num=gen_prompt_num, language=language)
     
    with open(memory_dir, 'w', encoding='utf8') as f:
        print(f'Successfully updated memory for {name}')
        json.dump(memory, f, ensure_ascii=False)
        
    return memory


if __name__ == '__main__':
    target_memory = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'memories', 'eng_memory_cases.json')
    summarize_memory(target_memory, language='en')