khoj 1.42.1.dev10__py3-none-any.whl → 1.42.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. khoj/configure.py +2 -0
  2. khoj/database/adapters/__init__.py +9 -7
  3. khoj/database/models/__init__.py +9 -9
  4. khoj/interface/compiled/404/index.html +2 -2
  5. khoj/interface/compiled/_next/static/chunks/{2117-5a41630a2bd2eae8.js → 2117-056a00add390772b.js} +1 -1
  6. khoj/interface/compiled/_next/static/chunks/7127-79a3af5138960272.js +1 -0
  7. khoj/interface/compiled/_next/static/chunks/{5138-2cce449fd2454abf.js → 7211-7fedd2ee3655239c.js} +1 -1
  8. khoj/interface/compiled/_next/static/chunks/app/agents/layout-1b6273baddb72146.js +1 -0
  9. khoj/interface/compiled/_next/static/chunks/app/automations/page-ef89ac958e78aa81.js +1 -0
  10. khoj/interface/compiled/_next/static/chunks/app/chat/page-d71351493e1f7c2b.js +1 -0
  11. khoj/interface/compiled/_next/static/chunks/app/{page-45ae5e99e8a61821.js → page-4bbe55de8b080c1f.js} +1 -1
  12. khoj/interface/compiled/_next/static/chunks/app/search/layout-4505b79deb734a30.js +1 -0
  13. khoj/interface/compiled/_next/static/chunks/app/share/chat/{page-9a167dc9b5fcd464.js → page-e3f49c25480e3be4.js} +1 -1
  14. khoj/interface/compiled/_next/static/chunks/{main-876327ac335776ab.js → main-63d6432f34cdf74b.js} +1 -1
  15. khoj/interface/compiled/_next/static/chunks/{webpack-964e8ed3380daff1.js → webpack-e4c73eaddc365142.js} +1 -1
  16. khoj/interface/compiled/_next/static/css/2b1cdb68b799b876.css +1 -0
  17. khoj/interface/compiled/_next/static/css/440ae0f0f650dc35.css +1 -0
  18. khoj/interface/compiled/_next/static/css/{9c223d337a984468.css → 7017ee76c2f2cd87.css} +1 -1
  19. khoj/interface/compiled/agents/index.html +2 -2
  20. khoj/interface/compiled/agents/index.txt +1 -1
  21. khoj/interface/compiled/automations/index.html +2 -2
  22. khoj/interface/compiled/automations/index.txt +2 -2
  23. khoj/interface/compiled/chat/index.html +2 -2
  24. khoj/interface/compiled/chat/index.txt +2 -2
  25. khoj/interface/compiled/index.html +2 -2
  26. khoj/interface/compiled/index.txt +2 -2
  27. khoj/interface/compiled/search/index.html +2 -2
  28. khoj/interface/compiled/search/index.txt +1 -1
  29. khoj/interface/compiled/settings/index.html +2 -2
  30. khoj/interface/compiled/settings/index.txt +1 -1
  31. khoj/interface/compiled/share/chat/index.html +2 -2
  32. khoj/interface/compiled/share/chat/index.txt +2 -2
  33. khoj/processor/conversation/anthropic/anthropic_chat.py +19 -134
  34. khoj/processor/conversation/anthropic/utils.py +1 -1
  35. khoj/processor/conversation/google/gemini_chat.py +20 -141
  36. khoj/processor/conversation/offline/chat_model.py +23 -153
  37. khoj/processor/conversation/openai/gpt.py +14 -128
  38. khoj/processor/conversation/prompts.py +2 -63
  39. khoj/processor/conversation/utils.py +94 -89
  40. khoj/processor/image/generate.py +16 -11
  41. khoj/processor/operator/__init__.py +2 -3
  42. khoj/processor/operator/operator_agent_binary.py +11 -11
  43. khoj/processor/operator/operator_environment_computer.py +2 -2
  44. khoj/processor/tools/online_search.py +9 -3
  45. khoj/processor/tools/run_code.py +5 -5
  46. khoj/routers/api.py +5 -527
  47. khoj/routers/api_automation.py +243 -0
  48. khoj/routers/api_chat.py +48 -129
  49. khoj/routers/helpers.py +373 -121
  50. khoj/routers/research.py +11 -43
  51. khoj/utils/helpers.py +0 -6
  52. {khoj-1.42.1.dev10.dist-info → khoj-1.42.2.dist-info}/METADATA +3 -3
  53. {khoj-1.42.1.dev10.dist-info → khoj-1.42.2.dist-info}/RECORD +58 -57
  54. khoj/interface/compiled/_next/static/chunks/7127-d3199617463d45f0.js +0 -1
  55. khoj/interface/compiled/_next/static/chunks/app/agents/layout-4e2a134ec26aa606.js +0 -1
  56. khoj/interface/compiled/_next/static/chunks/app/automations/page-465741d9149dfd48.js +0 -1
  57. khoj/interface/compiled/_next/static/chunks/app/chat/page-898079bcea5376f4.js +0 -1
  58. khoj/interface/compiled/_next/static/chunks/app/search/layout-c02531d586972d7d.js +0 -1
  59. khoj/interface/compiled/_next/static/css/76c658ee459140a9.css +0 -1
  60. khoj/interface/compiled/_next/static/css/fca983d49c3dd1a3.css +0 -1
  61. /khoj/interface/compiled/_next/static/{2niR8lV9_OpGs1vdb2yMp → BDHACq0ud8EERJ3YZ4aWo}/_buildManifest.js +0 -0
  62. /khoj/interface/compiled/_next/static/{2niR8lV9_OpGs1vdb2yMp → BDHACq0ud8EERJ3YZ4aWo}/_ssgManifest.js +0 -0
  63. {khoj-1.42.1.dev10.dist-info → khoj-1.42.2.dist-info}/WHEEL +0 -0
  64. {khoj-1.42.1.dev10.dist-info → khoj-1.42.2.dist-info}/entry_points.txt +0 -0
  65. {khoj-1.42.1.dev10.dist-info → khoj-1.42.2.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,243 @@
1
+ import json
2
+ import logging
3
+ import threading
4
+ from typing import Optional
5
+
6
+ import cron_descriptor
7
+ import pytz
8
+ from apscheduler.job import Job
9
+ from apscheduler.triggers.cron import CronTrigger
10
+ from fastapi import APIRouter, Request
11
+ from fastapi.responses import Response
12
+ from starlette.authentication import requires
13
+
14
+ from khoj.database.adapters import AutomationAdapters, ConversationAdapters
15
+ from khoj.database.models import KhojUser
16
+ from khoj.processor.conversation.utils import clean_json
17
+ from khoj.routers.helpers import schedule_automation, schedule_query
18
+ from khoj.utils.helpers import is_none_or_empty
19
+
20
+ # Initialize Router
21
+ api_automation = APIRouter()
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ @api_automation.get("", response_class=Response)
26
+ @requires(["authenticated"])
27
+ def get_automations(request: Request) -> Response:
28
+ user: KhojUser = request.user.object
29
+
30
+ # Collate all automations created by user that are still active
31
+ automations_info = [automation_info for automation_info in AutomationAdapters.get_automations_metadata(user)]
32
+
33
+ # Return tasks information as a JSON response
34
+ return Response(content=json.dumps(automations_info), media_type="application/json", status_code=200)
35
+
36
+
37
+ @api_automation.delete("", response_class=Response)
38
+ @requires(["authenticated"])
39
+ def delete_automation(request: Request, automation_id: str) -> Response:
40
+ user: KhojUser = request.user.object
41
+
42
+ try:
43
+ automation_info = AutomationAdapters.delete_automation(user, automation_id)
44
+ except ValueError:
45
+ return Response(status_code=204)
46
+
47
+ # Return deleted automation information as a JSON response
48
+ return Response(content=json.dumps(automation_info), media_type="application/json", status_code=200)
49
+
50
+
51
+ @api_automation.post("", response_class=Response)
52
+ @requires(["authenticated"])
53
+ def post_automation(
54
+ request: Request,
55
+ q: str,
56
+ crontime: str,
57
+ subject: Optional[str] = None,
58
+ city: Optional[str] = None,
59
+ region: Optional[str] = None,
60
+ country: Optional[str] = None,
61
+ timezone: Optional[str] = None,
62
+ ) -> Response:
63
+ user: KhojUser = request.user.object
64
+
65
+ # Perform validation checks
66
+ if is_none_or_empty(q) or is_none_or_empty(crontime):
67
+ return Response(content="A query and crontime is required", status_code=400)
68
+ if not cron_descriptor.get_description(crontime):
69
+ return Response(content="Invalid crontime", status_code=400)
70
+
71
+ # Infer subject, query to run
72
+ _, query_to_run, generated_subject = schedule_query(q, chat_history=[], user=user)
73
+ subject = subject or generated_subject
74
+
75
+ # Normalize query parameters
76
+ # Add /automated_task prefix to query if not present
77
+ query_to_run = query_to_run.strip()
78
+ if not query_to_run.startswith("/automated_task"):
79
+ query_to_run = f"/automated_task {query_to_run}"
80
+
81
+ # Normalize crontime for AP Scheduler CronTrigger
82
+ crontime = crontime.strip()
83
+ if len(crontime.split(" ")) > 5:
84
+ # Truncate crontime to 5 fields
85
+ crontime = " ".join(crontime.split(" ")[:5])
86
+
87
+ # Convert crontime to standard unix crontime
88
+ crontime = crontime.replace("?", "*")
89
+
90
+ # Disallow minute level automation recurrence
91
+ minute_value = crontime.split(" ")[0]
92
+ if not minute_value.isdigit():
93
+ return Response(
94
+ content="Minute level recurrence is unsupported. Please create a less frequent schedule.",
95
+ status_code=400,
96
+ )
97
+
98
+ # Create new Conversation Session associated with this new task
99
+ title = f"Automation: {subject}"
100
+ conversation = ConversationAdapters.create_conversation_session(user, request.user.client_app, title=title)
101
+
102
+ # Schedule automation with query_to_run, timezone, subject directly provided by user
103
+ try:
104
+ # Use the query to run as the scheduling request if the scheduling request is unset
105
+ calling_url = request.url.replace(query=f"{request.url.query}")
106
+ automation = schedule_automation(
107
+ query_to_run, subject, crontime, timezone, q, user, calling_url, str(conversation.id)
108
+ )
109
+ except Exception as e:
110
+ logger.error(f"Error creating automation {q} for {user.email}: {e}", exc_info=True)
111
+ return Response(
112
+ content=f"Unable to create automation. Ensure the automation doesn't already exist.",
113
+ media_type="text/plain",
114
+ status_code=500,
115
+ )
116
+
117
+ # Collate info about the created user automation
118
+ automation_info = AutomationAdapters.get_automation_metadata(user, automation)
119
+
120
+ # Return information about the created automation as a JSON response
121
+ return Response(content=json.dumps(automation_info), media_type="application/json", status_code=200)
122
+
123
+
124
+ @api_automation.post("/trigger", response_class=Response)
125
+ @requires(["authenticated"])
126
+ def trigger_manual_job(
127
+ request: Request,
128
+ automation_id: str,
129
+ ):
130
+ user: KhojUser = request.user.object
131
+
132
+ # Check, get automation to edit
133
+ try:
134
+ automation: Job = AutomationAdapters.get_automation(user, automation_id)
135
+ except ValueError as e:
136
+ logger.error(f"Error triggering automation {automation_id} for {user.email}: {e}", exc_info=True)
137
+ return Response(content="Invalid automation", status_code=403)
138
+
139
+ # Trigger the job without waiting for the result.
140
+ scheduled_chat_func = automation.func
141
+
142
+ # Run the function in a separate thread
143
+ thread = threading.Thread(target=scheduled_chat_func, args=automation.args, kwargs=automation.kwargs)
144
+ thread.start()
145
+
146
+ return Response(content="Automation triggered", status_code=200)
147
+
148
+
149
+ @api_automation.put("", response_class=Response)
150
+ @requires(["authenticated"])
151
+ def edit_job(
152
+ request: Request,
153
+ automation_id: str,
154
+ q: Optional[str],
155
+ subject: Optional[str],
156
+ crontime: Optional[str],
157
+ city: Optional[str] = None,
158
+ region: Optional[str] = None,
159
+ country: Optional[str] = None,
160
+ timezone: Optional[str] = None,
161
+ ) -> Response:
162
+ user: KhojUser = request.user.object
163
+
164
+ # Perform validation checks
165
+ if is_none_or_empty(q) or is_none_or_empty(subject) or is_none_or_empty(crontime):
166
+ return Response(content="A query, subject and crontime is required", status_code=400)
167
+ if not cron_descriptor.get_description(crontime):
168
+ return Response(content="Invalid crontime", status_code=400)
169
+
170
+ # Check, get automation to edit
171
+ try:
172
+ automation: Job = AutomationAdapters.get_automation(user, automation_id)
173
+ except ValueError as e:
174
+ logger.error(f"Error editing automation {automation_id} for {user.email}: {e}", exc_info=True)
175
+ return Response(content="Invalid automation", status_code=403)
176
+
177
+ # Infer subject, query to run
178
+ _, query_to_run, _ = schedule_query(q, chat_history=[], user=user)
179
+ subject = subject
180
+
181
+ # Normalize query parameters
182
+ # Add /automated_task prefix to query if not present
183
+ query_to_run = query_to_run.strip()
184
+ if not query_to_run.startswith("/automated_task"):
185
+ query_to_run = f"/automated_task {query_to_run}"
186
+ # Normalize crontime for AP Scheduler CronTrigger
187
+ crontime = crontime.strip()
188
+ if len(crontime.split(" ")) > 5:
189
+ # Truncate crontime to 5 fields
190
+ crontime = " ".join(crontime.split(" ")[:5])
191
+ # Convert crontime to standard unix crontime
192
+ crontime = crontime.replace("?", "*")
193
+
194
+ # Disallow minute level automation recurrence
195
+ minute_value = crontime.split(" ")[0]
196
+ if not minute_value.isdigit():
197
+ return Response(
198
+ content="Recurrence of every X minutes is unsupported. Please create a less frequent schedule.",
199
+ status_code=400,
200
+ )
201
+
202
+ # Construct updated automation metadata
203
+ automation_metadata: dict[str, str] = json.loads(clean_json(automation.name))
204
+ automation_metadata["scheduling_request"] = q
205
+ automation_metadata["query_to_run"] = query_to_run
206
+ automation_metadata["subject"] = subject.strip()
207
+ automation_metadata["crontime"] = crontime
208
+ conversation_id = automation_metadata.get("conversation_id")
209
+
210
+ if not conversation_id:
211
+ title = f"Automation: {subject}"
212
+
213
+ # Create new Conversation Session associated with this new task
214
+ conversation = ConversationAdapters.create_conversation_session(user, request.user.client_app, title=title)
215
+
216
+ conversation_id = str(conversation.id)
217
+ automation_metadata["conversation_id"] = conversation_id
218
+
219
+ # Modify automation with updated query, subject
220
+ automation.modify(
221
+ name=json.dumps(automation_metadata),
222
+ kwargs={
223
+ "query_to_run": query_to_run,
224
+ "subject": subject,
225
+ "scheduling_request": q,
226
+ "user": user,
227
+ "calling_url": request.url,
228
+ "conversation_id": conversation_id,
229
+ },
230
+ )
231
+
232
+ # Reschedule automation if crontime updated
233
+ user_timezone = pytz.timezone(timezone)
234
+ trigger = CronTrigger.from_crontab(crontime, user_timezone)
235
+ if automation.trigger != trigger:
236
+ automation.reschedule(trigger=trigger)
237
+
238
+ # Collate info about the updated user automation
239
+ automation = AutomationAdapters.get_automation(user, automation.id)
240
+ automation_info = AutomationAdapters.get_automation_metadata(user, automation)
241
+
242
+ # Return modified automation information as a JSON response
243
+ return Response(content=json.dumps(automation_info), media_type="application/json", status_code=200)
khoj/routers/api_chat.py CHANGED
@@ -40,7 +40,6 @@ from khoj.processor.tools.online_search import (
40
40
  search_online,
41
41
  )
42
42
  from khoj.processor.tools.run_code import run_code
43
- from khoj.routers.api import extract_references_and_questions
44
43
  from khoj.routers.email import send_query_feedback
45
44
  from khoj.routers.helpers import (
46
45
  ApiImageRateLimiter,
@@ -63,6 +62,7 @@ from khoj.routers.helpers import (
63
62
  is_query_empty,
64
63
  is_ready_to_chat,
65
64
  read_chat_stream,
65
+ search_documents,
66
66
  update_telemetry_state,
67
67
  validate_chat_model,
68
68
  )
@@ -752,7 +752,7 @@ async def chat(
752
752
  q,
753
753
  chat_response="",
754
754
  user=user,
755
- meta_log=meta_log,
755
+ chat_history=chat_history,
756
756
  compiled_references=compiled_references,
757
757
  online_results=online_results,
758
758
  code_results=code_results,
@@ -918,7 +918,7 @@ async def chat(
918
918
  if city or region or country or country_code:
919
919
  location = LocationData(city=city, region=region, country=country, country_code=country_code)
920
920
  user_message_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
921
- meta_log = conversation.conversation_log
921
+ chat_history = conversation.messages
922
922
 
923
923
  # If interrupt flag is set, wait for the previous turn to be saved before proceeding
924
924
  if interrupt_flag:
@@ -964,14 +964,14 @@ async def chat(
964
964
  operator_results = [OperatorRun(**iter_dict) for iter_dict in last_message.operatorContext or []]
965
965
  train_of_thought = [thought.model_dump() for thought in last_message.trainOfThought or []]
966
966
  # Drop the interrupted message from conversation history
967
- meta_log["chat"].pop()
967
+ chat_history.pop()
968
968
  logger.info(f"Loaded interrupted partial context from conversation {conversation_id}.")
969
969
 
970
970
  if conversation_commands == [ConversationCommand.Default]:
971
971
  try:
972
972
  chosen_io = await aget_data_sources_and_output_format(
973
973
  q,
974
- meta_log,
974
+ chat_history,
975
975
  is_automated_task,
976
976
  user=user,
977
977
  query_images=uploaded_images,
@@ -1011,7 +1011,7 @@ async def chat(
1011
1011
  user=user,
1012
1012
  query=defiltered_query,
1013
1013
  conversation_id=conversation_id,
1014
- conversation_history=meta_log,
1014
+ conversation_history=conversation.messages,
1015
1015
  previous_iterations=list(research_results),
1016
1016
  query_images=uploaded_images,
1017
1017
  agent=agent,
@@ -1055,115 +1055,13 @@ async def chat(
1055
1055
  if state.verbose > 1:
1056
1056
  logger.debug(f'Researched Results: {"".join(r.summarizedResult for r in research_results)}')
1057
1057
 
1058
- used_slash_summarize = conversation_commands == [ConversationCommand.Summarize]
1059
- # Skip trying to summarize if
1060
- if (
1061
- # summarization intent was inferred
1062
- ConversationCommand.Summarize in conversation_commands
1063
- # and not triggered via slash command
1064
- and not used_slash_summarize
1065
- # but we can't actually summarize
1066
- and len(file_filters) == 0
1067
- ):
1068
- conversation_commands.remove(ConversationCommand.Summarize)
1069
- elif ConversationCommand.Summarize in conversation_commands:
1070
- response_log = ""
1071
- agent_has_entries = await EntryAdapters.aagent_has_entries(agent)
1072
- if len(file_filters) == 0 and not agent_has_entries:
1073
- response_log = "No files selected for summarization. Please add files using the section on the left."
1074
- async for result in send_llm_response(response_log, tracer.get("usage")):
1075
- yield result
1076
- else:
1077
- async for response in generate_summary_from_files(
1078
- q=q,
1079
- user=user,
1080
- file_filters=file_filters,
1081
- meta_log=meta_log,
1082
- query_images=uploaded_images,
1083
- agent=agent,
1084
- send_status_func=partial(send_event, ChatEvent.STATUS),
1085
- query_files=attached_file_context,
1086
- tracer=tracer,
1087
- ):
1088
- if isinstance(response, dict) and ChatEvent.STATUS in response:
1089
- yield response[ChatEvent.STATUS]
1090
- else:
1091
- if isinstance(response, str):
1092
- response_log = response
1093
- async for result in send_llm_response(response, tracer.get("usage")):
1094
- yield result
1095
-
1096
- summarized_document = FileAttachment(
1097
- name="Summarized Document",
1098
- content=response_log,
1099
- type="text/plain",
1100
- size=len(response_log.encode("utf-8")),
1101
- )
1102
-
1103
- async for result in send_event(ChatEvent.GENERATED_ASSETS, {"files": [summarized_document.model_dump()]}):
1104
- yield result
1105
-
1106
- generated_files.append(summarized_document)
1107
-
1108
- custom_filters = []
1109
- if conversation_commands == [ConversationCommand.Help]:
1110
- if not q:
1111
- chat_model = await ConversationAdapters.aget_user_chat_model(user)
1112
- if chat_model == None:
1113
- chat_model = await ConversationAdapters.aget_default_chat_model(user)
1114
- model_type = chat_model.model_type
1115
- formatted_help = help_message.format(model=model_type, version=state.khoj_version, device=get_device())
1116
- async for result in send_llm_response(formatted_help, tracer.get("usage")):
1117
- yield result
1118
- return
1119
- # Adding specification to search online specifically on khoj.dev pages.
1120
- custom_filters.append("site:khoj.dev")
1121
- conversation_commands.append(ConversationCommand.Online)
1122
-
1123
- if ConversationCommand.Automation in conversation_commands:
1124
- try:
1125
- automation, crontime, query_to_run, subject = await create_automation(
1126
- q, timezone, user, request.url, meta_log, tracer=tracer
1127
- )
1128
- except Exception as e:
1129
- logger.error(f"Error scheduling task {q} for {user.email}: {e}")
1130
- error_message = f"Unable to create automation. Ensure the automation doesn't already exist."
1131
- async for result in send_llm_response(error_message, tracer.get("usage")):
1132
- yield result
1133
- return
1134
-
1135
- llm_response = construct_automation_created_message(automation, crontime, query_to_run, subject)
1136
- # Trigger task to save conversation to DB
1137
- asyncio.create_task(
1138
- save_to_conversation_log(
1139
- q,
1140
- llm_response,
1141
- user,
1142
- meta_log,
1143
- user_message_time,
1144
- intent_type="automation",
1145
- client_application=request.user.client_app,
1146
- conversation_id=conversation_id,
1147
- inferred_queries=[query_to_run],
1148
- automation_id=automation.id,
1149
- query_images=uploaded_images,
1150
- train_of_thought=train_of_thought,
1151
- raw_query_files=raw_query_files,
1152
- tracer=tracer,
1153
- )
1154
- )
1155
- # Send LLM Response
1156
- async for result in send_llm_response(llm_response, tracer.get("usage")):
1157
- yield result
1158
- return
1159
-
1160
1058
  # Gather Context
1161
1059
  ## Extract Document References
1162
1060
  if not ConversationCommand.Research in conversation_commands:
1163
1061
  try:
1164
- async for result in extract_references_and_questions(
1062
+ async for result in search_documents(
1165
1063
  user,
1166
- meta_log,
1064
+ chat_history,
1167
1065
  q,
1168
1066
  (n or 7),
1169
1067
  d,
@@ -1212,11 +1110,11 @@ async def chat(
1212
1110
  try:
1213
1111
  async for result in search_online(
1214
1112
  defiltered_query,
1215
- meta_log,
1113
+ chat_history,
1216
1114
  location,
1217
1115
  user,
1218
1116
  partial(send_event, ChatEvent.STATUS),
1219
- custom_filters,
1117
+ custom_filters=[],
1220
1118
  max_online_searches=3,
1221
1119
  query_images=uploaded_images,
1222
1120
  query_files=attached_file_context,
@@ -1240,7 +1138,7 @@ async def chat(
1240
1138
  try:
1241
1139
  async for result in read_webpages(
1242
1140
  defiltered_query,
1243
- meta_log,
1141
+ chat_history,
1244
1142
  location,
1245
1143
  user,
1246
1144
  partial(send_event, ChatEvent.STATUS),
@@ -1281,7 +1179,7 @@ async def chat(
1281
1179
  context = f"# Iteration 1:\n#---\nNotes:\n{compiled_references}\n\nOnline Results:{online_results}"
1282
1180
  async for result in run_code(
1283
1181
  defiltered_query,
1284
- meta_log,
1182
+ chat_history,
1285
1183
  context,
1286
1184
  location,
1287
1185
  user,
@@ -1306,7 +1204,7 @@ async def chat(
1306
1204
  async for result in operate_environment(
1307
1205
  defiltered_query,
1308
1206
  user,
1309
- meta_log,
1207
+ chat_history,
1310
1208
  location,
1311
1209
  list(operator_results)[-1] if operator_results else None,
1312
1210
  query_images=uploaded_images,
@@ -1356,7 +1254,7 @@ async def chat(
1356
1254
  async for result in text_to_image(
1357
1255
  defiltered_query,
1358
1256
  user,
1359
- meta_log,
1257
+ chat_history,
1360
1258
  location_data=location,
1361
1259
  references=compiled_references,
1362
1260
  online_results=online_results,
@@ -1400,7 +1298,7 @@ async def chat(
1400
1298
 
1401
1299
  async for result in generate_mermaidjs_diagram(
1402
1300
  q=defiltered_query,
1403
- conversation_history=meta_log,
1301
+ chat_history=chat_history,
1404
1302
  location_data=location,
1405
1303
  note_references=compiled_references,
1406
1304
  online_results=online_results,
@@ -1456,40 +1354,36 @@ async def chat(
1456
1354
 
1457
1355
  llm_response, chat_metadata = await agenerate_chat_response(
1458
1356
  defiltered_query,
1459
- meta_log,
1357
+ chat_history,
1460
1358
  conversation,
1461
1359
  compiled_references,
1462
1360
  online_results,
1463
1361
  code_results,
1464
1362
  operator_results,
1465
1363
  research_results,
1466
- inferred_queries,
1467
- conversation_commands,
1468
1364
  user,
1469
- request.user.client_app,
1470
1365
  location,
1471
1366
  user_name,
1472
1367
  uploaded_images,
1473
- train_of_thought,
1474
1368
  attached_file_context,
1475
- raw_query_files,
1476
- generated_images,
1477
1369
  generated_files,
1478
- generated_mermaidjs_diagram,
1479
1370
  program_execution_context,
1480
1371
  generated_asset_results,
1481
1372
  is_subscribed,
1482
1373
  tracer,
1483
1374
  )
1484
1375
 
1376
+ full_response = ""
1485
1377
  async for item in llm_response:
1486
- # Should not happen with async generator, end is signaled by loop exit. Skip.
1487
- if item is None:
1378
+ # Should not happen with async generator. Skip.
1379
+ if item is None or not isinstance(item, ResponseWithThought):
1380
+ logger.warning(f"Unexpected item type in LLM response: {type(item)}. Skipping.")
1488
1381
  continue
1489
1382
  if cancellation_event.is_set():
1490
1383
  break
1491
- message = item.response if isinstance(item, ResponseWithThought) else item
1492
- if isinstance(item, ResponseWithThought) and item.thought:
1384
+ message = item.response
1385
+ full_response += message if message else ""
1386
+ if item.thought:
1493
1387
  async for result in send_event(ChatEvent.THOUGHT, item.thought):
1494
1388
  yield result
1495
1389
  continue
@@ -1506,6 +1400,31 @@ async def chat(
1506
1400
  logger.warning(f"Error during streaming. Stopping send: {e}")
1507
1401
  break
1508
1402
 
1403
+ # Save conversation once finish streaming
1404
+ asyncio.create_task(
1405
+ save_to_conversation_log(
1406
+ q,
1407
+ chat_response=full_response,
1408
+ user=user,
1409
+ chat_history=chat_history,
1410
+ compiled_references=compiled_references,
1411
+ online_results=online_results,
1412
+ code_results=code_results,
1413
+ operator_results=operator_results,
1414
+ research_results=research_results,
1415
+ inferred_queries=inferred_queries,
1416
+ client_application=request.user.client_app,
1417
+ conversation_id=str(conversation.id),
1418
+ query_images=uploaded_images,
1419
+ train_of_thought=train_of_thought,
1420
+ raw_query_files=raw_query_files,
1421
+ generated_images=generated_images,
1422
+ raw_generated_files=generated_files,
1423
+ generated_mermaidjs_diagram=generated_mermaidjs_diagram,
1424
+ tracer=tracer,
1425
+ )
1426
+ )
1427
+
1509
1428
  # Signal end of LLM response after the loop finishes
1510
1429
  if not cancellation_event.is_set():
1511
1430
  async for result in send_event(ChatEvent.END_LLM_RESPONSE, ""):