23
23
API_URL = API_BASE + "/api/answer"
24
24
API_KEY = os .getenv ("API_KEY" )
25
25
TOKEN = os .getenv ("TELEGRAM_BOT_TOKEN" )
26
+ API_CONTEXT_MESSAGES_COUNT = 20 # Number of messages (10 pairs) to use for API context
26
27
27
28
# --- Storage Configuration ---
28
29
STORAGE_TYPE = os .getenv ("STORAGE_TYPE" , "memory" ) # Default to in-memory
39
40
if STORAGE_TYPE .lower () == "mongodb" :
40
41
if not MONGODB_URI :
41
42
logger .error ("STORAGE_TYPE is 'mongodb' but MONGODB_URI is not set. Exiting." )
42
- exit (1 ) # Or fallback to memory: STORAGE_TYPE = "memory"; logger.warning(...)
43
+ exit (1 )
43
44
try :
44
45
logger .info (f"Attempting to connect to MongoDB: { MONGODB_URI [:15 ]} ... DB: { MONGODB_DB_NAME } " )
45
- mongo_client = MongoClient (MONGODB_URI , serverSelectionTimeoutMS = 5000 ) # 5 second timeout
46
- mongo_client .admin .command ('ismaster' ) # Check connection
46
+ mongo_client = MongoClient (MONGODB_URI , serverSelectionTimeoutMS = 5000 )
47
+ mongo_client .admin .command ('ismaster' )
47
48
db = mongo_client [MONGODB_DB_NAME ]
48
49
mongo_collection = db [MONGODB_COLLECTION_NAME ]
49
50
logger .info (f"Successfully connected to MongoDB and selected collection '{ MONGODB_COLLECTION_NAME } '." )
@@ -71,47 +72,44 @@ async def get_chat_data(chat_id: int) -> dict:
71
72
"""
72
73
Fetches chat history, conversation ID, and user info from the configured storage.
73
74
The user info stored pertains to the *last known user* who interacted in this chat.
75
+ History fetched is the complete history.
74
76
"""
75
77
chat_id_str = str (chat_id )
76
- # Default structure now includes user_info
77
78
default_data = {"history" : [], "conversation_id" : None , "user_info" : None }
78
79
79
80
if STORAGE_TYPE == "mongodb" and mongo_collection is not None :
80
81
try :
81
82
doc = mongo_collection .find_one ({"_id" : chat_id_str })
82
-
83
83
if doc :
84
84
history = doc .get ("conversation_history" , [])
85
85
conv_id = doc .get ("conversation_id" , None )
86
- user_info = doc .get ("user_info" , None ) # Get user info
86
+ user_info = doc .get ("user_info" , None )
87
87
return {"history" : history , "conversation_id" : conv_id , "user_info" : user_info }
88
88
else :
89
89
return default_data
90
90
except Exception as e :
91
91
logger .error (f"MongoDB Error fetching data for chat_id { chat_id_str } : { e } " , exc_info = True )
92
- return default_data # Return default on error
92
+ return default_data
93
93
else :
94
- # Use .get with default for user_info for backward compatibility if key missing
95
94
data = in_memory_storage .get (chat_id_str , default_data )
96
95
return {
97
96
"history" : data .get ("history" , []),
98
97
"conversation_id" : data .get ("conversation_id" , None ),
99
- "user_info" : data .get ("user_info" , None ) # Get user info
98
+ "user_info" : data .get ("user_info" , None )
100
99
}
101
100
102
101
103
102
async def save_chat_data (chat_id : int , history : list , conversation_id : str | None , user_info : dict | None ):
104
103
"""
105
- Saves chat history, conversation ID, and user info to the configured storage.
104
+ Saves the complete chat history, conversation ID, and user info to the configured storage.
106
105
"""
107
106
chat_id_str = str (chat_id )
108
- max_history_len_pairs = 10 # Store last 10 pairs (20 messages)
109
- limited_history = history [- (max_history_len_pairs * 2 ):]
107
+ # The 'history' argument is the full history, which will be saved entirely.
110
108
111
109
if STORAGE_TYPE == "mongodb" and mongo_collection is not None :
112
110
try :
113
111
update_data = {
114
- "conversation_history" : limited_history ,
112
+ "conversation_history" : history , # Save the full history
115
113
"conversation_id" : conversation_id ,
116
114
}
117
115
if user_info :
@@ -124,23 +122,23 @@ async def save_chat_data(chat_id: int, history: list, conversation_id: str | Non
124
122
mongo_collection .update_one (
125
123
{"_id" : chat_id_str },
126
124
update_doc ,
127
- upsert = True # Create document if it doesn't exist
125
+ upsert = True
128
126
)
129
- logger .debug (f"Saved data for chat { chat_id_str } with user_info: { bool (user_info )} " )
127
+ logger .debug (f"Saved full history for chat { chat_id_str } with user_info: { bool (user_info )} " )
130
128
except Exception as e :
131
129
logger .error (f"MongoDB Error saving data for chat_id { chat_id_str } : { e } " , exc_info = True )
132
- else :
130
+ else : # in-memory storage
133
131
if chat_id_str not in in_memory_storage :
134
- in_memory_storage [chat_id_str ] = {} # Initialize if new
132
+ in_memory_storage [chat_id_str ] = {}
135
133
136
- in_memory_storage [chat_id_str ].update ({ # Use update to merge data
137
- "history" : limited_history ,
134
+ in_memory_storage [chat_id_str ].update ({
135
+ "history" : history , # Save the full history
138
136
"conversation_id" : conversation_id ,
139
- "last_updated" : datetime .datetime .now (datetime .timezone .utc ) # Timestamp for memory store
137
+ "last_updated" : datetime .datetime .now (datetime .timezone .utc )
140
138
})
141
139
if user_info :
142
140
in_memory_storage [chat_id_str ]["user_info" ] = user_info
143
- logger .debug (f"Saved in-memory data for chat { chat_id_str } with user_info: { bool (user_info )} " )
141
+ logger .debug (f"Saved full in-memory history for chat { chat_id_str } with user_info: { bool (user_info )} " )
144
142
145
143
146
144
async def start (update : Update , context : ContextTypes .DEFAULT_TYPE ) -> None :
@@ -161,17 +159,23 @@ async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE) -> No
161
159
await update .message .reply_text (help_text )
162
160
163
161
async def generate_answer (question : str , messages : list , conversation_id : str | None ) -> dict :
164
- """Generates an answer using the external DocsGPT API."""
162
+ """
163
+ Generates an answer using the external DocsGPT API.
164
+ Uses only the last API_CONTEXT_MESSAGES_COUNT messages for context.
165
+ """
165
166
if not API_KEY :
166
167
logger .warning ("API_KEY is not set. Cannot call DocsGPT API." )
167
168
return {"answer" : "Error: Backend API key is not configured." , "conversation_id" : conversation_id }
168
169
170
+ # Use only the last API_CONTEXT_MESSAGES_COUNT messages for the API call context
171
+ context_messages = messages [- API_CONTEXT_MESSAGES_COUNT :]
172
+
169
173
try :
170
- formatted_history = format_history_for_api (messages )
174
+ formatted_history = format_history_for_api (context_messages ) # Use limited context_messages
171
175
history_json = json .dumps (formatted_history )
172
176
except TypeError as e :
173
- logger .error (f"Failed to serialize history to JSON: { e } . History: { messages } " , exc_info = True )
174
- history_json = json .dumps ([])
177
+ logger .error (f"Failed to serialize history to JSON: { e } . History slice : { context_messages } " , exc_info = True )
178
+ history_json = json .dumps ([]) # Fallback to empty history for API
175
179
176
180
payload = {
177
181
"question" : question ,
@@ -182,13 +186,13 @@ async def generate_answer(question: str, messages: list, conversation_id: str |
182
186
headers = {
183
187
"Content-Type" : "application/json; charset=utf-8"
184
188
}
185
- timeout = 120.0 # Set a reasonable timeout for the API call
189
+ timeout = 120.0
186
190
default_error_msg = "Sorry, I couldn't get an answer from the backend service."
187
191
188
192
try :
189
193
async with httpx .AsyncClient () as client :
190
194
response = await client .post (API_URL , json = payload , headers = headers , timeout = timeout )
191
- response .raise_for_status () # Raise HTTPStatusError for 4xx/5xx
195
+ response .raise_for_status ()
192
196
193
197
data = response .json ()
194
198
answer = data .get ("answer" , default_error_msg )
@@ -208,7 +212,7 @@ async def generate_answer(question: str, messages: list, conversation_id: str |
208
212
logger .error (f"Network error calling DocsGPT API: { exc } " )
209
213
return {"answer" : f"{ default_error_msg } (Network Error)" , "conversation_id" : conversation_id }
210
214
except json .JSONDecodeError as exc :
211
- logger .error (f"Failed to decode JSON response from DocsGPT API: { exc } . Response text: { response .text } " , exc_info = True ) # Log response text
215
+ logger .error (f"Failed to decode JSON response from DocsGPT API: { exc } . Response text: { response .text } " , exc_info = True )
212
216
return {"answer" : f"{ default_error_msg } (Invalid Response Format)" , "conversation_id" : conversation_id }
213
217
except Exception as e :
214
218
logger .error (f"Unexpected error in generate_answer: { e } " , exc_info = True )
@@ -219,20 +223,18 @@ async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
219
223
"""Handles non-command messages: get history, query API, save history & user info, reply."""
220
224
if not update .message or not update .message .text or not update .effective_chat or not update .effective_user :
221
225
logger .warning ("Echo handler received an update without message, text, chat, or user." )
222
- return # Ignore updates without essential info
226
+ return
223
227
224
228
chat_id = update .effective_chat .id
225
229
user = update .effective_user
226
230
question = update .message .text
227
231
logger .info (f"Received message from user { user .id } ({ user .username or user .first_name } ) in chat_id { chat_id } " )
228
232
229
- # --- Send Typing Action ---
230
233
try :
231
234
await context .bot .send_chat_action (chat_id = chat_id , action = ChatAction .TYPING )
232
235
logger .debug (f"Sent typing action to chat { chat_id } " )
233
236
except Exception as e :
234
237
logger .warning (f"Failed to send typing action to chat { chat_id } : { e } " )
235
- # This is non-critical, we can continue processing even if typing doesn't show
236
238
237
239
user_info_dict = {
238
240
"id" : user .id ,
@@ -244,22 +246,23 @@ async def echo(update: Update, context: ContextTypes.DEFAULT_TYPE) -> None:
244
246
}
245
247
user_info_dict = {k : v for k , v in user_info_dict .items () if v is not None }
246
248
249
+ # Get full history
247
250
chat_data = await get_chat_data (chat_id )
248
- current_history = chat_data ["history" ]
251
+ current_history = chat_data ["history" ] # This is the full history
249
252
current_conversation_id = chat_data ["conversation_id" ]
250
253
251
254
current_history .append ({"role" : "user" , "content" : question })
252
255
253
- # This is the potentially long-running call that we show typing for
256
+ # generate_answer will use a slice of current_history for API context
254
257
response_doc = await generate_answer (question , current_history , current_conversation_id )
255
258
answer = response_doc ["answer" ]
256
- new_conversation_id = response_doc ["conversation_id" ] # Use the ID returned by API
259
+ new_conversation_id = response_doc ["conversation_id" ]
257
260
258
261
current_history .append ({"role" : "assistant" , "content" : answer })
259
262
263
+ # Save the full, updated current_history
260
264
await save_chat_data (chat_id , current_history , new_conversation_id , user_info_dict )
261
265
262
- # Sending the reply will automatically stop the typing indicator
263
266
try :
264
267
await update .message .reply_text (answer , parse_mode = ParseMode .MARKDOWN )
265
268
except Exception as e :
@@ -279,18 +282,15 @@ def format_history_for_api(messages: list) -> list:
279
282
api_history = []
280
283
i = 0
281
284
while i < len (messages ):
282
- # Look for a user message
283
285
if messages [i ].get ("role" ) == "user" and "content" in messages [i ]:
284
286
prompt_content = messages [i ]["content" ]
285
287
response_content = None
286
- # Check if the next message is a corresponding assistant response
287
288
if i + 1 < len (messages ) and messages [i + 1 ].get ("role" ) == "assistant" and "content" in messages [i + 1 ]:
288
289
response_content = messages [i + 1 ]["content" ]
289
- # Add the pair to history
290
290
api_history .append ({"prompt" : prompt_content , "response" : response_content })
291
- i += 2 # Move past both user and assistant message
291
+ i += 2
292
292
else :
293
- i += 1 # Move past the user message only
293
+ i += 1
294
294
else :
295
295
i += 1
296
296
return api_history
0 commit comments