khoj 1.42.9.dev27__py3-none-any.whl → 1.42.10__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 (56) hide show
  1. khoj/database/adapters/__init__.py +0 -20
  2. khoj/database/models/__init__.py +0 -1
  3. khoj/interface/compiled/404/index.html +2 -2
  4. khoj/interface/compiled/_next/static/chunks/app/agents/layout-4e2a134ec26aa606.js +1 -0
  5. khoj/interface/compiled/_next/static/chunks/app/chat/layout-ad4d1792ab1a4108.js +1 -0
  6. khoj/interface/compiled/_next/static/chunks/app/chat/page-e3b6206ca5190c32.js +1 -0
  7. khoj/interface/compiled/_next/static/chunks/{webpack-cfa752859e991115.js → webpack-15412ee214acd999.js} +1 -1
  8. khoj/interface/compiled/_next/static/css/{c34713c98384ee87.css → 821d0d60b0b6871d.css} +1 -1
  9. khoj/interface/compiled/agents/index.html +2 -2
  10. khoj/interface/compiled/agents/index.txt +1 -1
  11. khoj/interface/compiled/automations/index.html +2 -2
  12. khoj/interface/compiled/automations/index.txt +1 -1
  13. khoj/interface/compiled/chat/index.html +2 -2
  14. khoj/interface/compiled/chat/index.txt +2 -2
  15. khoj/interface/compiled/index.html +2 -2
  16. khoj/interface/compiled/index.txt +1 -1
  17. khoj/interface/compiled/search/index.html +2 -2
  18. khoj/interface/compiled/search/index.txt +1 -1
  19. khoj/interface/compiled/settings/index.html +2 -2
  20. khoj/interface/compiled/settings/index.txt +1 -1
  21. khoj/interface/compiled/share/chat/index.html +2 -2
  22. khoj/interface/compiled/share/chat/index.txt +1 -1
  23. khoj/processor/content/markdown/markdown_to_entries.py +9 -38
  24. khoj/processor/content/org_mode/org_to_entries.py +2 -18
  25. khoj/processor/content/org_mode/orgnode.py +16 -18
  26. khoj/processor/content/text_to_entries.py +0 -30
  27. khoj/processor/conversation/anthropic/anthropic_chat.py +2 -11
  28. khoj/processor/conversation/anthropic/utils.py +103 -90
  29. khoj/processor/conversation/google/gemini_chat.py +1 -4
  30. khoj/processor/conversation/google/utils.py +18 -80
  31. khoj/processor/conversation/offline/chat_model.py +3 -3
  32. khoj/processor/conversation/openai/gpt.py +38 -13
  33. khoj/processor/conversation/openai/utils.py +12 -113
  34. khoj/processor/conversation/prompts.py +35 -17
  35. khoj/processor/conversation/utils.py +58 -129
  36. khoj/processor/operator/grounding_agent.py +1 -1
  37. khoj/processor/operator/operator_agent_binary.py +3 -4
  38. khoj/processor/tools/online_search.py +0 -18
  39. khoj/processor/tools/run_code.py +1 -1
  40. khoj/routers/api_chat.py +1 -1
  41. khoj/routers/helpers.py +27 -297
  42. khoj/routers/research.py +155 -169
  43. khoj/search_type/text_search.py +0 -2
  44. khoj/utils/helpers.py +8 -284
  45. khoj/utils/initialization.py +2 -0
  46. khoj/utils/rawconfig.py +0 -11
  47. {khoj-1.42.9.dev27.dist-info → khoj-1.42.10.dist-info}/METADATA +1 -1
  48. {khoj-1.42.9.dev27.dist-info → khoj-1.42.10.dist-info}/RECORD +53 -53
  49. khoj/interface/compiled/_next/static/chunks/app/agents/layout-e00fb81dca656a10.js +0 -1
  50. khoj/interface/compiled/_next/static/chunks/app/chat/layout-33934fc2d6ae6838.js +0 -1
  51. khoj/interface/compiled/_next/static/chunks/app/chat/page-ef738950ea1babc3.js +0 -1
  52. /khoj/interface/compiled/_next/static/{i4QM4-da6IM0xYbLu3B8H → P0Niz53SXQbBiZBs-WnaS}/_buildManifest.js +0 -0
  53. /khoj/interface/compiled/_next/static/{i4QM4-da6IM0xYbLu3B8H → P0Niz53SXQbBiZBs-WnaS}/_ssgManifest.js +0 -0
  54. {khoj-1.42.9.dev27.dist-info → khoj-1.42.10.dist-info}/WHEEL +0 -0
  55. {khoj-1.42.9.dev27.dist-info → khoj-1.42.10.dist-info}/entry_points.txt +0 -0
  56. {khoj-1.42.9.dev27.dist-info → khoj-1.42.10.dist-info}/licenses/LICENSE +0 -0
@@ -58,7 +58,7 @@ def makelist_with_filepath(filename):
58
58
  return makelist(f, filename)
59
59
 
60
60
 
61
- def makelist(file, filename, start_line: int = 1, ancestry_lines: int = 0) -> List["Orgnode"]:
61
+ def makelist(file, filename) -> List["Orgnode"]:
62
62
  """
63
63
  Read an org-mode file and return a list of Orgnode objects
64
64
  created from this file.
@@ -66,7 +66,7 @@ def makelist(file, filename, start_line: int = 1, ancestry_lines: int = 0) -> Li
66
66
  ctr = 0
67
67
 
68
68
  if type(file) == str:
69
- f = file.splitlines()
69
+ f = file.split("\n")
70
70
  else:
71
71
  f = file
72
72
 
@@ -114,23 +114,14 @@ def makelist(file, filename, start_line: int = 1, ancestry_lines: int = 0) -> Li
114
114
  logbook = list()
115
115
  thisNode.properties = property_map
116
116
  nodelist.append(thisNode)
117
- # Account for ancestry lines that were prepended when calculating line numbers
118
- if ancestry_lines > 0:
119
- calculated_line = start_line + ctr - 1 - ancestry_lines
120
- if calculated_line <= 0:
121
- calculated_line = 1 # Fallback to line 1 if calculation results in invalid line number
122
- else:
123
- calculated_line = start_line + ctr - 1
124
- if calculated_line <= 0:
125
- calculated_line = ctr # Use the original behavior if start_line calculation fails
126
- property_map = {"LINE": f"file://{normalize_filename(filename)}#line={calculated_line}"}
117
+ property_map = {"LINE": f"file:{normalize_filename(filename)}::{ctr}"}
127
118
  previous_level = level
128
119
  previous_heading: str = heading
129
120
  level = heading_search.group(1)
130
121
  heading = heading_search.group(2)
131
122
  bodytext = ""
132
123
  tags = list() # set of all tags in headline
133
- tag_search = re.search(r"(.*?)\s+:([a-zA-Z0-9@_].*?):\s*$", heading)
124
+ tag_search = re.search(r"(.*?)\s*:([a-zA-Z0-9].*?):$", heading)
134
125
  if tag_search:
135
126
  heading = tag_search.group(1)
136
127
  parsedtags = tag_search.group(2)
@@ -269,6 +260,14 @@ def makelist(file, filename, start_line: int = 1, ancestry_lines: int = 0) -> Li
269
260
  # Prefix filepath/title to ancestors
270
261
  n.ancestors = [file_title] + n.ancestors
271
262
 
263
+ # Set SOURCE property to a file+heading based org-mode link to the entry
264
+ if n.level == 0:
265
+ n.properties["LINE"] = f"file:{normalize_filename(filename)}::0"
266
+ n.properties["SOURCE"] = f"[[file:{normalize_filename(filename)}]]"
267
+ else:
268
+ escaped_heading = n.heading.replace("[", "\\[").replace("]", "\\]")
269
+ n.properties["SOURCE"] = f"[[file:{normalize_filename(filename)}::*{escaped_heading}]]"
270
+
272
271
  return nodelist
273
272
 
274
273
 
@@ -521,11 +520,10 @@ class Orgnode(object):
521
520
  n = n + "\n"
522
521
 
523
522
  # Output Property Drawer
524
- if self._properties:
525
- n = n + indent + ":PROPERTIES:\n"
526
- for key, value in self._properties.items():
527
- n = n + indent + f":{key}: {value}\n"
528
- n = n + indent + ":END:\n"
523
+ n = n + indent + ":PROPERTIES:\n"
524
+ for key, value in self._properties.items():
525
+ n = n + indent + f":{key}: {value}\n"
526
+ n = n + indent + ":END:\n"
529
527
 
530
528
  # Output Body
531
529
  if self.hasBody:
@@ -81,35 +81,8 @@ class TextToEntries(ABC):
81
81
  chunked_entry_chunks = text_splitter.split_text(entry.compiled)
82
82
  corpus_id = uuid.uuid4()
83
83
 
84
- line_start = None
85
- last_offset = 0
86
- if entry.uri and entry.uri.startswith("file://"):
87
- if "#line=" in entry.uri:
88
- line_start = int(entry.uri.split("#line=", 1)[-1].split("&", 1)[0])
89
- else:
90
- line_start = 0
91
-
92
84
  # Create heading prefixed entry from each chunk
93
85
  for chunk_index, compiled_entry_chunk in enumerate(chunked_entry_chunks):
94
- # set line start in uri of chunked entries
95
- entry_uri = entry.uri
96
- if line_start is not None:
97
- # Find the chunk in the raw text to get an accurate line number.
98
- # Search for the unmodified chunk from the last offset.
99
- searchable_chunk = compiled_entry_chunk.strip()
100
- if searchable_chunk:
101
- chunk_start_pos_in_raw = entry.raw.find(searchable_chunk, last_offset)
102
- if chunk_start_pos_in_raw != -1:
103
- # Found the chunk. Calculate its line offset from the start of the raw text.
104
- line_offset_in_raw = entry.raw[:chunk_start_pos_in_raw].count("\n")
105
- new_line_num = line_start + line_offset_in_raw
106
- entry_uri = re.sub(r"#line=\d+", f"#line={new_line_num}", entry.uri)
107
- # Update search position for the next chunk to start after the current one.
108
- last_offset = chunk_start_pos_in_raw + len(searchable_chunk)
109
- else:
110
- # Chunk not found in raw text, likely from a heading. Use original line_start.
111
- entry_uri = re.sub(r"#line=\d+", f"#line={line_start}", entry.uri)
112
-
113
86
  # Prepend heading to all other chunks, the first chunk already has heading from original entry
114
87
  if chunk_index > 0 and entry.heading:
115
88
  # Snip heading to avoid crossing max_tokens limit
@@ -126,7 +99,6 @@ class TextToEntries(ABC):
126
99
  entry.raw = compiled_entry_chunk if raw_is_compiled else TextToEntries.clean_field(entry.raw)
127
100
  entry.heading = TextToEntries.clean_field(entry.heading)
128
101
  entry.file = TextToEntries.clean_field(entry.file)
129
- entry_uri = TextToEntries.clean_field(entry_uri)
130
102
 
131
103
  chunked_entries.append(
132
104
  Entry(
@@ -135,7 +107,6 @@ class TextToEntries(ABC):
135
107
  heading=entry.heading,
136
108
  file=entry.file,
137
109
  corpus_id=corpus_id,
138
- uri=entry_uri,
139
110
  )
140
111
  )
141
112
 
@@ -221,7 +192,6 @@ class TextToEntries(ABC):
221
192
  file_type=file_type,
222
193
  hashed_value=entry_hash,
223
194
  corpus_id=entry.corpus_id,
224
- url=entry.uri,
225
195
  search_model=model,
226
196
  file_object=file_object,
227
197
  )
@@ -22,20 +22,12 @@ logger = logging.getLogger(__name__)
22
22
 
23
23
 
24
24
  def anthropic_send_message_to_model(
25
- messages,
26
- api_key,
27
- api_base_url,
28
- model,
29
- response_type="text",
30
- response_schema=None,
31
- tools=None,
32
- deepthought=False,
33
- tracer={},
25
+ messages, api_key, api_base_url, model, response_type="text", response_schema=None, deepthought=False, tracer={}
34
26
  ):
35
27
  """
36
28
  Send message to model
37
29
  """
38
- # Get response from model. Don't use response_type because Anthropic doesn't support it.
30
+ # Get Response from GPT. Don't use response_type because Anthropic doesn't support it.
39
31
  return anthropic_completion_with_backoff(
40
32
  messages=messages,
41
33
  system_prompt="",
@@ -44,7 +36,6 @@ def anthropic_send_message_to_model(
44
36
  api_base_url=api_base_url,
45
37
  response_type=response_type,
46
38
  response_schema=response_schema,
47
- tools=tools,
48
39
  deepthought=deepthought,
49
40
  tracer=tracer,
50
41
  )
@@ -1,8 +1,9 @@
1
1
  import json
2
2
  import logging
3
3
  from copy import deepcopy
4
+ from textwrap import dedent
4
5
  from time import perf_counter
5
- from typing import AsyncGenerator, Dict, List
6
+ from typing import AsyncGenerator, Dict, List, Optional, Type
6
7
 
7
8
  import anthropic
8
9
  from langchain_core.messages.chat import ChatMessage
@@ -17,14 +18,11 @@ from tenacity import (
17
18
 
18
19
  from khoj.processor.conversation.utils import (
19
20
  ResponseWithThought,
20
- ToolCall,
21
21
  commit_conversation_trace,
22
22
  get_image_from_base64,
23
23
  get_image_from_url,
24
24
  )
25
25
  from khoj.utils.helpers import (
26
- ToolDefinition,
27
- create_tool_definition,
28
26
  get_anthropic_async_client,
29
27
  get_anthropic_client,
30
28
  get_chat_usage_metrics,
@@ -59,10 +57,9 @@ def anthropic_completion_with_backoff(
59
57
  max_tokens: int | None = None,
60
58
  response_type: str = "text",
61
59
  response_schema: BaseModel | None = None,
62
- tools: List[ToolDefinition] = None,
63
60
  deepthought: bool = False,
64
61
  tracer: dict = {},
65
- ) -> ResponseWithThought:
62
+ ) -> str:
66
63
  client = anthropic_clients.get(api_key)
67
64
  if not client:
68
65
  client = get_anthropic_client(api_key, api_base_url)
@@ -70,26 +67,12 @@ def anthropic_completion_with_backoff(
70
67
 
71
68
  formatted_messages, system = format_messages_for_anthropic(messages, system_prompt)
72
69
 
73
- thoughts = ""
74
70
  aggregated_response = ""
75
71
  final_message = None
76
72
  model_kwargs = model_kwargs or dict()
77
-
78
- # Configure structured output
79
- if tools:
80
- # Convert tools to Anthropic format
81
- model_kwargs["tools"] = [
82
- anthropic.types.ToolParam(name=tool.name, description=tool.description, input_schema=tool.schema)
83
- for tool in tools
84
- ]
85
- # Cache tool definitions
86
- last_tool = model_kwargs["tools"][-1]
87
- last_tool["cache_control"] = {"type": "ephemeral"}
88
- elif response_schema:
89
- tool = create_tool_definition(response_schema)
90
- model_kwargs["tools"] = [
91
- anthropic.types.ToolParam(name=tool.name, description=tool.description, input_schema=tool.schema)
92
- ]
73
+ if response_schema:
74
+ tool = create_anthropic_tool_definition(response_schema=response_schema)
75
+ model_kwargs["tools"] = [tool]
93
76
  elif response_type == "json_object" and not (is_reasoning_model(model_name) and deepthought):
94
77
  # Prefill model response with '{' to make it output a valid JSON object. Not supported with extended thinking.
95
78
  formatted_messages.append(anthropic.types.MessageParam(role="assistant", content="{"))
@@ -113,41 +96,15 @@ def anthropic_completion_with_backoff(
113
96
  max_tokens=max_tokens,
114
97
  **(model_kwargs),
115
98
  ) as stream:
116
- for chunk in stream:
117
- if chunk.type != "content_block_delta":
118
- continue
119
- if chunk.delta.type == "thinking_delta":
120
- thoughts += chunk.delta.thinking
121
- elif chunk.delta.type == "text_delta":
122
- aggregated_response += chunk.delta.text
99
+ for text in stream.text_stream:
100
+ aggregated_response += text
123
101
  final_message = stream.get_final_message()
124
102
 
125
- # Track raw content of model response to reuse for cache hits in multi-turn chats
126
- raw_content = [item.model_dump() for item in final_message.content]
127
-
128
- # Extract all tool calls if tools are enabled
129
- if tools:
130
- tool_calls = [
131
- ToolCall(name=item.name, args=item.input, id=item.id).__dict__
132
- for item in final_message.content
133
- if item.type == "tool_use"
134
- ]
135
- if tool_calls:
136
- # If there are tool calls, aggregate thoughts and responses into thoughts
137
- if thoughts and aggregated_response:
138
- # wrap each line of thought in italics
139
- thoughts = "\n".join([f"*{line.strip()}*" for line in thoughts.splitlines() if line.strip()])
140
- thoughts = f"{thoughts}\n\n{aggregated_response}"
141
- else:
142
- thoughts = thoughts or aggregated_response
143
- # Json dump tool calls into aggregated response
144
- aggregated_response = json.dumps(tool_calls)
145
- # If response schema is used, return the first tool call's input
146
- elif response_schema:
147
- for item in final_message.content:
148
- if item.type == "tool_use":
149
- aggregated_response = json.dumps(item.input)
150
- break
103
+ # Extract first tool call from final message
104
+ for item in final_message.content:
105
+ if item.type == "tool_use":
106
+ aggregated_response = json.dumps(item.input)
107
+ break
151
108
 
152
109
  # Calculate cost of chat
153
110
  input_tokens = final_message.usage.input_tokens
@@ -169,7 +126,7 @@ def anthropic_completion_with_backoff(
169
126
  if is_promptrace_enabled():
170
127
  commit_conversation_trace(messages, aggregated_response, tracer)
171
128
 
172
- return ResponseWithThought(text=aggregated_response, thought=thoughts, raw_content=raw_content)
129
+ return aggregated_response
173
130
 
174
131
 
175
132
  @retry(
@@ -226,10 +183,10 @@ async def anthropic_chat_completion_with_backoff(
226
183
  if chunk.type == "message_delta":
227
184
  if chunk.delta.stop_reason == "refusal":
228
185
  yield ResponseWithThought(
229
- text="...I'm sorry, but my safety filters prevent me from assisting with this query."
186
+ response="...I'm sorry, but my safety filters prevent me from assisting with this query."
230
187
  )
231
188
  elif chunk.delta.stop_reason == "max_tokens":
232
- yield ResponseWithThought(text="...I'm sorry, but I've hit my response length limit.")
189
+ yield ResponseWithThought(response="...I'm sorry, but I've hit my response length limit.")
233
190
  if chunk.delta.stop_reason in ["refusal", "max_tokens"]:
234
191
  logger.warning(
235
192
  f"LLM Response Prevented for {model_name}: {chunk.delta.stop_reason}.\n"
@@ -242,7 +199,7 @@ async def anthropic_chat_completion_with_backoff(
242
199
  # Handle streamed response chunk
243
200
  response_chunk: ResponseWithThought = None
244
201
  if chunk.delta.type == "text_delta":
245
- response_chunk = ResponseWithThought(text=chunk.delta.text)
202
+ response_chunk = ResponseWithThought(response=chunk.delta.text)
246
203
  aggregated_response += chunk.delta.text
247
204
  if chunk.delta.type == "thinking_delta":
248
205
  response_chunk = ResponseWithThought(thought=chunk.delta.thinking)
@@ -275,14 +232,13 @@ async def anthropic_chat_completion_with_backoff(
275
232
  commit_conversation_trace(messages, aggregated_response, tracer)
276
233
 
277
234
 
278
- def format_messages_for_anthropic(raw_messages: list[ChatMessage], system_prompt: str = None):
235
+ def format_messages_for_anthropic(messages: list[ChatMessage], system_prompt: str = None):
279
236
  """
280
237
  Format messages for Anthropic
281
238
  """
282
239
  # Extract system prompt
283
240
  system_prompt = system_prompt or ""
284
- messages = deepcopy(raw_messages)
285
- for message in messages:
241
+ for message in messages.copy():
286
242
  if message.role == "system":
287
243
  if isinstance(message.content, list):
288
244
  system_prompt += "\n".join([part["text"] for part in message.content if part["type"] == "text"])
@@ -294,30 +250,15 @@ def format_messages_for_anthropic(raw_messages: list[ChatMessage], system_prompt
294
250
  else:
295
251
  system = None
296
252
 
297
- # Anthropic requires the first message to be a user message unless its a tool call
298
- message_type = messages[0].additional_kwargs.get("message_type", None)
299
- if len(messages) == 1 and message_type != "tool_call":
253
+ # Anthropic requires the first message to be a 'user' message
254
+ if len(messages) == 1:
300
255
  messages[0].role = "user"
256
+ elif len(messages) > 1 and messages[0].role == "assistant":
257
+ messages = messages[1:]
301
258
 
259
+ # Convert image urls to base64 encoded images in Anthropic message format
302
260
  for message in messages:
303
- # Handle tool call and tool result message types from additional_kwargs
304
- message_type = message.additional_kwargs.get("message_type")
305
- if message_type == "tool_call":
306
- pass
307
- elif message_type == "tool_result":
308
- # Convert tool_result to Anthropic tool_result format
309
- content = []
310
- for part in message.content:
311
- content.append(
312
- {
313
- "type": "tool_result",
314
- "tool_use_id": part["id"],
315
- "content": part["content"],
316
- }
317
- )
318
- message.content = content
319
- # Convert image urls to base64 encoded images in Anthropic message format
320
- elif isinstance(message.content, list):
261
+ if isinstance(message.content, list):
321
262
  content = []
322
263
  # Sort the content. Anthropic models prefer that text comes after images.
323
264
  message.content.sort(key=lambda x: 0 if x["type"] == "image_url" else 1)
@@ -363,15 +304,18 @@ def format_messages_for_anthropic(raw_messages: list[ChatMessage], system_prompt
363
304
  if isinstance(block, dict) and "cache_control" in block:
364
305
  del block["cache_control"]
365
306
 
366
- # Add cache control to the last content block of last message.
367
- # Caching should improve research efficiency.
368
- cache_message = messages[-1]
307
+ # Add cache control to the last content block of second to last message.
308
+ # In research mode, this message content is list of iterations, updated after each research iteration.
309
+ # Caching it should improve research efficiency.
310
+ cache_message = messages[-2]
369
311
  if isinstance(cache_message.content, list) and cache_message.content:
370
312
  # Add cache control to the last content block only if it's a text block with non-empty content
371
313
  last_block = cache_message.content[-1]
372
- if isinstance(last_block, dict) and (
373
- (last_block.get("type") == "text" and last_block.get("text", "").strip())
374
- or (last_block.get("type") == "tool_result" and last_block.get("content", []))
314
+ if (
315
+ isinstance(last_block, dict)
316
+ and last_block.get("type") == "text"
317
+ and last_block.get("text")
318
+ and last_block.get("text").strip()
375
319
  ):
376
320
  last_block["cache_control"] = {"type": "ephemeral"}
377
321
 
@@ -382,5 +326,74 @@ def format_messages_for_anthropic(raw_messages: list[ChatMessage], system_prompt
382
326
  return formatted_messages, system
383
327
 
384
328
 
329
+ def create_anthropic_tool_definition(
330
+ response_schema: Type[BaseModel],
331
+ tool_name: str = None,
332
+ tool_description: Optional[str] = None,
333
+ ) -> anthropic.types.ToolParam:
334
+ """
335
+ Converts a response schema BaseModel class into an Anthropic tool definition dictionary.
336
+
337
+ This format is expected by Anthropic's API when defining tools the model can use.
338
+
339
+ Args:
340
+ response_schema: The Pydantic BaseModel class to convert.
341
+ This class defines the response schema for the tool.
342
+ tool_name: The name for the Anthropic tool (e.g., "get_weather", "plan_next_step").
343
+ tool_description: Optional description for the Anthropic tool.
344
+ If None, it attempts to use the Pydantic model's docstring.
345
+ If that's also missing, a fallback description is generated.
346
+
347
+ Returns:
348
+ An tool definition for Anthropic's API.
349
+ """
350
+ model_schema = response_schema.model_json_schema()
351
+
352
+ name = tool_name or response_schema.__name__.lower()
353
+ description = tool_description
354
+ if description is None:
355
+ docstring = response_schema.__doc__
356
+ if docstring:
357
+ description = dedent(docstring).strip()
358
+ else:
359
+ # Fallback description if no explicit one or docstring is provided
360
+ description = f"Tool named '{name}' accepts specified parameters."
361
+
362
+ # Process properties to inline enums and remove $defs dependency
363
+ processed_properties = {}
364
+ original_properties = model_schema.get("properties", {})
365
+ defs = model_schema.get("$defs", {})
366
+
367
+ for prop_name, prop_schema in original_properties.items():
368
+ current_prop_schema = deepcopy(prop_schema) # Work on a copy
369
+ # Check for enums defined directly in the property for simpler direct enum definitions.
370
+ if "$ref" in current_prop_schema:
371
+ ref_path = current_prop_schema["$ref"]
372
+ if ref_path.startswith("#/$defs/"):
373
+ def_name = ref_path.split("/")[-1]
374
+ if def_name in defs and "enum" in defs[def_name]:
375
+ enum_def = defs[def_name]
376
+ current_prop_schema["enum"] = enum_def["enum"]
377
+ current_prop_schema["type"] = enum_def.get("type", "string")
378
+ if "description" not in current_prop_schema and "description" in enum_def:
379
+ current_prop_schema["description"] = enum_def["description"]
380
+ del current_prop_schema["$ref"] # Remove the $ref as it's been inlined
381
+
382
+ processed_properties[prop_name] = current_prop_schema
383
+
384
+ # The input_schema for Anthropic tools is a JSON Schema object.
385
+ # Pydantic's model_json_schema() provides most of what's needed.
386
+ input_schema = {
387
+ "type": "object",
388
+ "properties": processed_properties,
389
+ }
390
+
391
+ # Include 'required' fields if specified in the Pydantic model
392
+ if "required" in model_schema and model_schema["required"]:
393
+ input_schema["required"] = model_schema["required"]
394
+
395
+ return anthropic.types.ToolParam(name=name, description=description, input_schema=input_schema)
396
+
397
+
385
398
  def is_reasoning_model(model_name: str) -> bool:
386
399
  return any(model_name.startswith(model) for model in REASONING_MODELS)
@@ -28,7 +28,6 @@ def gemini_send_message_to_model(
28
28
  api_base_url=None,
29
29
  response_type="text",
30
30
  response_schema=None,
31
- tools=None,
32
31
  model_kwargs=None,
33
32
  deepthought=False,
34
33
  tracer={},
@@ -38,10 +37,8 @@ def gemini_send_message_to_model(
38
37
  """
39
38
  model_kwargs = {}
40
39
 
41
- if tools:
42
- model_kwargs["tools"] = tools
43
40
  # Monitor for flakiness in 1.5+ models. This would cause unwanted behavior and terminate response early in 1.5 models.
44
- elif response_type == "json_object" and not model.startswith("gemini-1.5"):
41
+ if response_type == "json_object" and not model.startswith("gemini-1.5"):
45
42
  model_kwargs["response_mime_type"] = "application/json"
46
43
  if response_schema:
47
44
  model_kwargs["response_schema"] = response_schema