khoj 1.42.9.dev27__py3-none-any.whl → 1.42.10.dev2__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.
- khoj/database/adapters/__init__.py +0 -20
- khoj/database/models/__init__.py +0 -1
- khoj/interface/compiled/404/index.html +2 -2
- khoj/interface/compiled/_next/static/chunks/app/agents/layout-4e2a134ec26aa606.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/agents/{page-9a4610474cd59a71.js → page-5db6ad18da10d353.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/automations/{page-f7bb9d777b7745d4.js → page-6271e2e31c7571d1.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/layout-ad4d1792ab1a4108.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/chat/page-4c6b873a4a5c7d2f.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/{page-2b3056cba8aa96ce.js → page-a19a597629e87fb8.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/search/layout-f5881c7ae3ba0795.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/search/{page-4885df3cd175c957.js → page-fa366ac14b228688.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/settings/{page-8be3b35178abf2ec.js → page-8f9a85f96088c18b.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/app/share/chat/layout-abb6c5f4239ad7be.js +1 -0
- khoj/interface/compiled/_next/static/chunks/app/share/chat/{page-4a4b0c0f4749c2b2.js → page-ed7787cf4938b8e3.js} +1 -1
- khoj/interface/compiled/_next/static/chunks/{webpack-cfa752859e991115.js → webpack-92ce8aaf95718ec4.js} +1 -1
- khoj/interface/compiled/_next/static/css/{e6da1287d41f5409.css → 02f60900b0d89ec7.css} +1 -1
- khoj/interface/compiled/_next/static/css/{c34713c98384ee87.css → 93eeacc43e261162.css} +1 -1
- khoj/interface/compiled/agents/index.html +2 -2
- khoj/interface/compiled/agents/index.txt +2 -2
- khoj/interface/compiled/automations/index.html +2 -2
- khoj/interface/compiled/automations/index.txt +2 -2
- khoj/interface/compiled/chat/index.html +2 -2
- khoj/interface/compiled/chat/index.txt +2 -2
- khoj/interface/compiled/index.html +2 -2
- khoj/interface/compiled/index.txt +2 -2
- khoj/interface/compiled/search/index.html +2 -2
- khoj/interface/compiled/search/index.txt +2 -2
- khoj/interface/compiled/settings/index.html +2 -2
- khoj/interface/compiled/settings/index.txt +2 -2
- khoj/interface/compiled/share/chat/index.html +2 -2
- khoj/interface/compiled/share/chat/index.txt +2 -2
- khoj/processor/content/markdown/markdown_to_entries.py +9 -38
- khoj/processor/content/org_mode/org_to_entries.py +2 -18
- khoj/processor/content/org_mode/orgnode.py +16 -18
- khoj/processor/content/text_to_entries.py +0 -30
- khoj/processor/conversation/anthropic/anthropic_chat.py +2 -11
- khoj/processor/conversation/anthropic/utils.py +103 -90
- khoj/processor/conversation/google/gemini_chat.py +1 -4
- khoj/processor/conversation/google/utils.py +18 -80
- khoj/processor/conversation/offline/chat_model.py +3 -3
- khoj/processor/conversation/openai/gpt.py +38 -13
- khoj/processor/conversation/openai/utils.py +12 -113
- khoj/processor/conversation/prompts.py +35 -17
- khoj/processor/conversation/utils.py +58 -129
- khoj/processor/operator/grounding_agent.py +1 -1
- khoj/processor/operator/operator_agent_binary.py +3 -4
- khoj/processor/tools/online_search.py +0 -18
- khoj/processor/tools/run_code.py +1 -1
- khoj/routers/api_chat.py +1 -1
- khoj/routers/helpers.py +27 -297
- khoj/routers/research.py +155 -169
- khoj/search_type/text_search.py +0 -2
- khoj/utils/helpers.py +8 -284
- khoj/utils/initialization.py +2 -0
- khoj/utils/rawconfig.py +0 -11
- {khoj-1.42.9.dev27.dist-info → khoj-1.42.10.dev2.dist-info}/METADATA +1 -1
- {khoj-1.42.9.dev27.dist-info → khoj-1.42.10.dev2.dist-info}/RECORD +62 -62
- khoj/interface/compiled/_next/static/chunks/app/agents/layout-e00fb81dca656a10.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/layout-33934fc2d6ae6838.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/chat/page-ef738950ea1babc3.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/search/layout-c02531d586972d7d.js +0 -1
- khoj/interface/compiled/_next/static/chunks/app/share/chat/layout-e8e5db7830bf3f47.js +0 -1
- /khoj/interface/compiled/_next/static/{i4QM4-da6IM0xYbLu3B8H → cuzJcS32_a4L4a6gCZ63y}/_buildManifest.js +0 -0
- /khoj/interface/compiled/_next/static/{i4QM4-da6IM0xYbLu3B8H → cuzJcS32_a4L4a6gCZ63y}/_ssgManifest.js +0 -0
- {khoj-1.42.9.dev27.dist-info → khoj-1.42.10.dev2.dist-info}/WHEEL +0 -0
- {khoj-1.42.9.dev27.dist-info → khoj-1.42.10.dev2.dist-info}/entry_points.txt +0 -0
- {khoj-1.42.9.dev27.dist-info → khoj-1.42.10.dev2.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
|
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.
|
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
|
-
|
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
|
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
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
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
|
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
|
-
) ->
|
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
|
-
|
79
|
-
|
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
|
117
|
-
|
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
|
-
#
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
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
|
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
|
-
|
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(
|
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(
|
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(
|
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
|
-
|
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
|
298
|
-
|
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
|
-
|
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
|
-
#
|
368
|
-
|
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
|
373
|
-
(last_block
|
374
|
-
|
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
|
-
|
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
|