mirror of
https://github.com/coalaura/whiskr.git
synced 2025-09-08 17:06:42 +00:00
better search tools
This commit is contained in:
10
.example.env
10
.example.env
@@ -1,5 +1,11 @@
|
||||
# Your openrouter.ai token
|
||||
# Your openrouter.ai token (required)
|
||||
OPENROUTER_TOKEN = ""
|
||||
|
||||
# How many messages/tool calls before the model is cut-off
|
||||
# Your exa-search token (optional)
|
||||
EXA_TOKEN = ""
|
||||
|
||||
# How many messages/tool calls before the model is cut-off (optional, default: 3)
|
||||
MAX_ITERATIONS = 3
|
||||
|
||||
# Replace unicode quotes, dashes, etc. in the assistants output (optional, default: false)
|
||||
CLEAN_CONTENT = true
|
||||
|
51
chat.go
51
chat.go
@@ -17,6 +17,7 @@ type ToolCall struct {
|
||||
Name string `json:"name"`
|
||||
Args string `json:"args"`
|
||||
Result string `json:"result,omitempty"`
|
||||
Done bool `json:"done,omitempty"`
|
||||
}
|
||||
|
||||
type Message struct {
|
||||
@@ -98,9 +99,11 @@ func (r *Request) Parse() (*openrouter.ChatCompletionRequest, error) {
|
||||
}
|
||||
}
|
||||
|
||||
if model.Tools && r.Search {
|
||||
request.Tools = GetSearchTool()
|
||||
if model.Tools && r.Search && ExaToken != "" {
|
||||
request.Tools = GetSearchTools()
|
||||
request.ToolChoice = "auto"
|
||||
|
||||
request.Messages = append(request.Messages, openrouter.SystemMessage("You have access to web search tools. Use `search_web` with `query` (string) and `num_results` (1-10) to find current information and get result summaries. Use `fetch_contents` with `urls` (array) to read full page content. Always specify all parameters for each tool call. Call only one tool per response."))
|
||||
}
|
||||
|
||||
prompt, err := BuildPrompt(r.Prompt, model)
|
||||
@@ -148,6 +151,8 @@ func (r *Request) Parse() (*openrouter.ChatCompletionRequest, error) {
|
||||
}
|
||||
|
||||
func HandleChat(w http.ResponseWriter, r *http.Request) {
|
||||
debug("new chat")
|
||||
|
||||
var raw Request
|
||||
|
||||
if err := json.NewDecoder(r.Body).Decode(&raw); err != nil {
|
||||
@@ -169,9 +174,6 @@ func HandleChat(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
request.Stream = true
|
||||
|
||||
// DEBUG
|
||||
dump(request)
|
||||
|
||||
response, err := NewStream(w)
|
||||
if err != nil {
|
||||
RespondJson(w, http.StatusBadRequest, map[string]any{
|
||||
@@ -181,12 +183,20 @@ func HandleChat(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
debug("handling request")
|
||||
|
||||
ctx := r.Context()
|
||||
|
||||
for iteration := range MaxIterations {
|
||||
debug("iteration %d of %d", iteration+1, MaxIterations)
|
||||
|
||||
if iteration == MaxIterations-1 {
|
||||
debug("no more tool calls")
|
||||
|
||||
request.Tools = nil
|
||||
request.ToolChoice = ""
|
||||
|
||||
request.Messages = append(request.Messages, openrouter.SystemMessage("You have reached the maximum number of tool calls for this conversation. Provide your final response based on the information you have gathered."))
|
||||
}
|
||||
|
||||
tool, message, err := RunCompletion(ctx, response, request)
|
||||
@@ -196,19 +206,39 @@ func HandleChat(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
if tool == nil || tool.Name != "search_internet" {
|
||||
if tool == nil {
|
||||
debug("no tool call, done")
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
debug("got %q tool call", tool.Name)
|
||||
|
||||
response.Send(ToolChunk(tool))
|
||||
|
||||
err = HandleSearchTool(ctx, tool)
|
||||
if err != nil {
|
||||
response.Send(ErrorChunk(err))
|
||||
switch tool.Name {
|
||||
case "search_web":
|
||||
err = HandleSearchWebTool(ctx, tool)
|
||||
if err != nil {
|
||||
response.Send(ErrorChunk(err))
|
||||
|
||||
return
|
||||
}
|
||||
case "fetch_contents":
|
||||
err = HandleFetchContentsTool(ctx, tool)
|
||||
if err != nil {
|
||||
response.Send(ErrorChunk(err))
|
||||
|
||||
return
|
||||
}
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
tool.Done = true
|
||||
|
||||
debug("finished tool call")
|
||||
|
||||
response.Send(ToolChunk(tool))
|
||||
|
||||
request.Messages = append(request.Messages,
|
||||
@@ -260,9 +290,6 @@ func RunCompletion(ctx context.Context, response *Stream, request *openrouter.Ch
|
||||
|
||||
choice := chunk.Choices[0]
|
||||
|
||||
// DEBUG
|
||||
debug(choice)
|
||||
|
||||
if choice.FinishReason == openrouter.FinishReasonContentFilter {
|
||||
response.Send(ErrorChunk(errors.New("stopped due to content_filter")))
|
||||
|
||||
|
20
clean.go
Normal file
20
clean.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package main
|
||||
|
||||
import "strings"
|
||||
|
||||
var cleaner = strings.NewReplacer(
|
||||
"‑", "-",
|
||||
"—", "-",
|
||||
|
||||
"“", "\"",
|
||||
"”", "\"",
|
||||
"’", "'",
|
||||
)
|
||||
|
||||
func CleanChunk(chunk string) string {
|
||||
if !CleanContent {
|
||||
return chunk
|
||||
}
|
||||
|
||||
return cleaner.Replace(chunk)
|
||||
}
|
16
debug.go
16
debug.go
@@ -1,23 +1,17 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
)
|
||||
|
||||
func dump(v any) {
|
||||
func debug(format string, args ...any) {
|
||||
if !Debug {
|
||||
return
|
||||
}
|
||||
|
||||
b, _ := json.MarshalIndent(v, "", "\t")
|
||||
os.WriteFile("debug.json", b, 0644)
|
||||
log.Debugf(format+"\n", args...)
|
||||
}
|
||||
|
||||
func debug(v any) {
|
||||
if !Debug {
|
||||
func debugIf(cond bool, format string, args ...any) {
|
||||
if !cond {
|
||||
return
|
||||
}
|
||||
|
||||
log.Debugf("%#v\n", v)
|
||||
debug(format, args)
|
||||
}
|
||||
|
23
env.go
23
env.go
@@ -10,16 +10,29 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
Debug bool
|
||||
MaxIterations int
|
||||
Debug bool
|
||||
|
||||
CleanContent bool
|
||||
MaxIterations int
|
||||
|
||||
OpenRouterToken string
|
||||
ExaToken string
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.MustPanic(godotenv.Load())
|
||||
|
||||
// enable debug logs & prints
|
||||
Debug = os.Getenv("DEBUG") == "true"
|
||||
|
||||
if Debug {
|
||||
log.Warning("Debug mode enabled")
|
||||
}
|
||||
|
||||
// de-ai assistant response content
|
||||
CleanContent = os.Getenv("DEBUG") == "true"
|
||||
|
||||
// maximum amount of iterations per turn
|
||||
if env := os.Getenv("MAX_ITERATIONS"); env != "" {
|
||||
iterations, err := strconv.Atoi(env)
|
||||
if err != nil {
|
||||
@@ -35,11 +48,13 @@ func init() {
|
||||
MaxIterations = 3
|
||||
}
|
||||
|
||||
// openrouter token used for all completions & model list
|
||||
if OpenRouterToken = os.Getenv("OPENROUTER_TOKEN"); OpenRouterToken == "" {
|
||||
log.Panic(errors.New("missing openrouter token"))
|
||||
}
|
||||
|
||||
if Debug {
|
||||
log.Warning("Debug mode enabled")
|
||||
// optional exa token used for search tools
|
||||
if ExaToken = os.Getenv("EXA_TOKEN"); ExaToken == "" {
|
||||
log.Warning("missing exa token, web search unavailable")
|
||||
}
|
||||
}
|
||||
|
131
exa.go
Normal file
131
exa.go
Normal file
@@ -0,0 +1,131 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type ExaResult struct {
|
||||
Title string `json:"title"`
|
||||
URL string `json:"url"`
|
||||
PublishedDate string `json:"publishedDate"`
|
||||
|
||||
Text string `json:"text"`
|
||||
Summary string `json:"summary"`
|
||||
}
|
||||
|
||||
type ExaResults struct {
|
||||
RequestID string `json:"requestId"`
|
||||
Results []ExaResult `json:"results"`
|
||||
}
|
||||
|
||||
func (e *ExaResult) String() string {
|
||||
var (
|
||||
label string
|
||||
text string
|
||||
)
|
||||
|
||||
if e.Text != "" {
|
||||
label = "Text"
|
||||
text = e.Text
|
||||
} else if e.Summary != "" {
|
||||
label = "Summary"
|
||||
text = e.Summary
|
||||
}
|
||||
|
||||
return fmt.Sprintf(
|
||||
"Title: %s \nURL: %s \nPublished Date: %s \n%s: %s",
|
||||
e.Title,
|
||||
e.URL,
|
||||
e.PublishedDate,
|
||||
label,
|
||||
strings.TrimSpace(text),
|
||||
)
|
||||
}
|
||||
|
||||
func (e *ExaResults) String() string {
|
||||
list := make([]string, len(e.Results))
|
||||
|
||||
for i, result := range e.Results {
|
||||
list[i] = result.String()
|
||||
}
|
||||
|
||||
return strings.Join(list, "\n\n---\n\n")
|
||||
}
|
||||
|
||||
func NewExaRequest(ctx context.Context, path string, data any) (*http.Request, error) {
|
||||
buf, err := json.Marshal(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("POST", fmt.Sprintf("https://api.exa.ai%s", path), bytes.NewReader(buf))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("X-Api-Key", ExaToken)
|
||||
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func RunExaRequest(req *http.Request) (*ExaResults, error) {
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer resp.Body.Close()
|
||||
|
||||
var result ExaResults
|
||||
|
||||
err = json.NewDecoder(resp.Body).Decode(&result)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func ExaRunSearch(ctx context.Context, args SearchWebArguments) (*ExaResults, error) {
|
||||
data := map[string]any{
|
||||
"query": args.Query,
|
||||
"type": "auto",
|
||||
"numResults": args.NumResults,
|
||||
"contents": map[string]any{
|
||||
"summary": map[string]any{
|
||||
"query": "Summarize this page only with all information directly relevant to answering the user's question: include key facts, numbers, dates, names, definitions, steps, code or commands, and the page's stance or conclusion; omit fluff and unrelated sections.",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
req, err := NewExaRequest(ctx, "/search", data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return RunExaRequest(req)
|
||||
}
|
||||
|
||||
func ExaRunContents(ctx context.Context, args FetchContentsArguments) (*ExaResults, error) {
|
||||
data := map[string]any{
|
||||
"urls": args.URLs,
|
||||
"text": map[string]any{
|
||||
"maxCharacters": 8000,
|
||||
},
|
||||
}
|
||||
|
||||
req, err := NewExaRequest(ctx, "/contents", data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return RunExaRequest(req)
|
||||
}
|
4
go.mod
4
go.mod
@@ -6,7 +6,7 @@ require (
|
||||
github.com/coalaura/logger v1.5.1
|
||||
github.com/go-chi/chi/v5 v5.2.2
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/revrost/go-openrouter v0.1.11-0.20250804020417-b3d94f4f6b46
|
||||
github.com/revrost/go-openrouter v0.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -16,6 +16,6 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/rs/zerolog v1.34.0 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
)
|
||||
|
8
go.sum
8
go.sum
@@ -22,8 +22,8 @@ github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/revrost/go-openrouter v0.1.11-0.20250804020417-b3d94f4f6b46 h1:Ai/eskFY6VN+0kARZEE9l3ccbwvGB9CQ6/gJfafHQs0=
|
||||
github.com/revrost/go-openrouter v0.1.11-0.20250804020417-b3d94f4f6b46/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0=
|
||||
github.com/revrost/go-openrouter v0.2.1 h1:4BMQ6pgYeEJq9pLl7pFbwnBabmqgUa35hGRnVHqjpA4=
|
||||
github.com/revrost/go-openrouter v0.2.1/go.mod h1:ZH/UdpnDEdMmJwq8tbSTX1S5I07ee8KMlEYN4jmegU0=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
@@ -36,8 +36,8 @@ golang.org/x/exp v0.0.0-20220909182711-5c715a9e8561/go.mod h1:cyybsKvd6eL0RnXn6p
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
|
1
main.go
1
main.go
@@ -35,6 +35,7 @@ func main() {
|
||||
r.Get("/-/data", func(w http.ResponseWriter, r *http.Request) {
|
||||
RespondJson(w, http.StatusOK, map[string]any{
|
||||
"version": Version,
|
||||
"search": ExaToken != "",
|
||||
"models": models,
|
||||
})
|
||||
})
|
||||
|
@@ -40,6 +40,10 @@ type Generation struct {
|
||||
NumSearchResults *int `json:"num_search_results"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
openrouter.DisableLogs()
|
||||
}
|
||||
|
||||
func OpenRouterClient() *openrouter.Client {
|
||||
return openrouter.NewClient(OpenRouterToken)
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ Output Style
|
||||
- Answer directly first. Use short paragraphs or bullet lists; avoid heavy formatting.
|
||||
- Use fenced code blocks with language tags for code. Keep examples minimal, runnable, and focused on the user's goal.
|
||||
- Prefer plain text for math and notation; show only essential steps when helpful.
|
||||
- Wrap multi-line code in markdown code-blocks.
|
||||
|
||||
Quality Bar
|
||||
- Do not invent facts or sources. If uncertain or missing data, say so and propose next steps or what info would resolve it.
|
||||
|
@@ -1,28 +0,0 @@
|
||||
You are an AI Web Search Assistant. Your task is to take a user's query, perform a web search if necessary, and then synthesize the findings into a clear, structured, and informative summary. This summary will be used by another AI to answer the user.
|
||||
|
||||
Guidelines:
|
||||
1. **Analyze the Query:** Understand the core intent. If the query implies a need for recent information (e.g., "latest," "recent," "this year") or a specific type of source (e.g., "news," "research paper," "official documentation"), prioritize that in your search and synthesis.
|
||||
2. **Web Search:** Search the web to find relevant, up-to-date information. Focus on recent, up-to-date data.
|
||||
3. **Synthesize Results:**
|
||||
* Provide up to 3-5 of the most relevant search results.
|
||||
* For each result, include:
|
||||
* `Title:` The title of the webpage.
|
||||
* `URL:` The direct URL.
|
||||
* `Published Date:` (If available or inferable, format as YYYY-MM-DD. Omit if not found).
|
||||
* `Summary:` A concise 2-3 sentence summary of the key information from the page relevant to the query. Focus on extracting factual details and key takeaways.
|
||||
* Format each result clearly, separated by a blank line.
|
||||
4. **Conciseness and Information Density:** Aim for maximum relevant information. Avoid conversational fluff, opinions, or introductory/concluding remarks. Just provide the structured search findings.
|
||||
5. **No Direct Answer (Usually):** Your primary role is to provide summarized search results. Do not try to directly answer the user's original question in a conversational way unless the query is very simple and can be answered by a single, authoritative fact from the search. The other AI will handle the final conversational response.
|
||||
6. **If No Good Results:** If the search yields no relevant results, state "No specific relevant information found for the query."
|
||||
|
||||
Example Output Format:
|
||||
|
||||
Title: [Page Title]
|
||||
URL: [Page URL]
|
||||
Published Date: [YYYY-MM-DD] (Omit if not found)
|
||||
Summary: [Concise summary of key information relevant to the query.]
|
||||
|
||||
Title: [Page Title]
|
||||
URL: [Page URL]
|
||||
Published Date: [YYYY-MM-DD] (Omit if not found)
|
||||
Summary: [Concise summary of key information relevant to the query.]
|
103
search.go
103
search.go
@@ -10,29 +10,56 @@ import (
|
||||
"github.com/revrost/go-openrouter"
|
||||
)
|
||||
|
||||
type SearchArguments struct {
|
||||
Query string `json:"query"`
|
||||
type SearchWebArguments struct {
|
||||
Query string `json:"query"`
|
||||
NumResults int `json:"num_results"`
|
||||
}
|
||||
|
||||
var (
|
||||
//go:embed prompts/search.txt
|
||||
PromptSearch string
|
||||
)
|
||||
type FetchContentsArguments struct {
|
||||
URLs []string `json:"urls"`
|
||||
}
|
||||
|
||||
func GetSearchTool() []openrouter.Tool {
|
||||
func GetSearchTools() []openrouter.Tool {
|
||||
return []openrouter.Tool{
|
||||
{
|
||||
Type: openrouter.ToolTypeFunction,
|
||||
Function: &openrouter.FunctionDefinition{
|
||||
Name: "search_internet",
|
||||
Description: "Search the internet for current information.",
|
||||
Name: "search_web",
|
||||
Description: "Search the web via Exa in auto mode. Returns up to 10 results with short summaries.",
|
||||
Parameters: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"query"},
|
||||
"required": []string{"query", "num_results"},
|
||||
"properties": map[string]any{
|
||||
"query": map[string]string{
|
||||
"query": map[string]any{
|
||||
"type": "string",
|
||||
"description": "A concise and specific query string.",
|
||||
"description": "A concise, specific search query in natural language.",
|
||||
},
|
||||
"num_results": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "Number of results to return (1-10). Default 10.",
|
||||
"minimum": 1,
|
||||
"maximum": 10,
|
||||
},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
},
|
||||
Strict: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: openrouter.ToolTypeFunction,
|
||||
Function: &openrouter.FunctionDefinition{
|
||||
Name: "fetch_contents",
|
||||
Description: "Fetch page contents for one or more URLs via Exa /contents.",
|
||||
Parameters: map[string]any{
|
||||
"type": "object",
|
||||
"required": []string{"urls"},
|
||||
"properties": map[string]any{
|
||||
"urls": map[string]any{
|
||||
"type": "array",
|
||||
"description": "List of URLs (1..N) to fetch.",
|
||||
"items": map[string]any{"type": "string"},
|
||||
"minItems": 1,
|
||||
},
|
||||
},
|
||||
"additionalProperties": false,
|
||||
@@ -43,8 +70,8 @@ func GetSearchTool() []openrouter.Tool {
|
||||
}
|
||||
}
|
||||
|
||||
func HandleSearchTool(ctx context.Context, tool *ToolCall) error {
|
||||
var arguments SearchArguments
|
||||
func HandleSearchWebTool(ctx context.Context, tool *ToolCall) error {
|
||||
var arguments SearchWebArguments
|
||||
|
||||
err := json.Unmarshal([]byte(tool.Args), &arguments)
|
||||
if err != nil {
|
||||
@@ -55,30 +82,50 @@ func HandleSearchTool(ctx context.Context, tool *ToolCall) error {
|
||||
return errors.New("no search query")
|
||||
}
|
||||
|
||||
request := openrouter.ChatCompletionRequest{
|
||||
Model: "perplexity/sonar",
|
||||
Messages: []openrouter.ChatCompletionMessage{
|
||||
openrouter.SystemMessage(PromptSearch),
|
||||
openrouter.UserMessage(arguments.Query),
|
||||
},
|
||||
Temperature: 0.25,
|
||||
MaxTokens: 2048,
|
||||
}
|
||||
|
||||
response, err := OpenRouterRun(ctx, request)
|
||||
results, err := ExaRunSearch(ctx, arguments)
|
||||
if err != nil {
|
||||
tool.Result = fmt.Sprintf("error: %v", err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(response.Choices) == 0 {
|
||||
tool.Result = "error: failed to perform search"
|
||||
if len(results.Results) == 0 {
|
||||
tool.Result = "error: no search results"
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
tool.Result = response.Choices[0].Message.Content.Text
|
||||
tool.Result = results.String()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func HandleFetchContentsTool(ctx context.Context, tool *ToolCall) error {
|
||||
var arguments FetchContentsArguments
|
||||
|
||||
err := json.Unmarshal([]byte(tool.Args), &arguments)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(arguments.URLs) == 0 {
|
||||
return errors.New("no urls")
|
||||
}
|
||||
|
||||
results, err := ExaRunContents(ctx, arguments)
|
||||
if err != nil {
|
||||
tool.Result = fmt.Sprintf("error: %v", err)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(results.Results) == 0 {
|
||||
tool.Result = "error: no search results"
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
tool.Result = results.String()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@@ -284,8 +284,8 @@ body.loading #version {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.message.has-reasoning:not(.has-text) div.text,
|
||||
.message.has-tool:not(.has-text) div.text,
|
||||
.message.has-reasoning:not(.has-text):not(.errored) div.text,
|
||||
.message.has-tool:not(.has-text):not(.errored) div.text,
|
||||
.message:not(.has-tool) .tool,
|
||||
.message:not(.has-reasoning) .reasoning {
|
||||
display: none;
|
||||
|
@@ -65,9 +65,9 @@
|
||||
<label for="reasoning-tokens" title="Maximum amount of reasoning tokens"></label>
|
||||
<input id="reasoning-tokens" type="number" min="2" max="1" step="0.05" value="0.85" />
|
||||
</div>
|
||||
<div class="option group">
|
||||
<button id="json" class="none" title="Turn on structured json output"></button>
|
||||
<button id="search" title="Turn on web-search (openrouter built-in)"></button>
|
||||
<div class="option group none">
|
||||
<button id="json" title="Turn on structured json output"></button>
|
||||
<button id="search" title="Turn on search tools (search_web and fetch_contents)"></button>
|
||||
</div>
|
||||
<div class="option">
|
||||
<button id="scrolling" title="Turn on auto-scrolling"></button>
|
||||
|
@@ -21,6 +21,7 @@
|
||||
models = {};
|
||||
|
||||
let autoScrolling = false,
|
||||
searchAvailable = false,
|
||||
jsonMode = false,
|
||||
searchTool = false;
|
||||
|
||||
@@ -441,6 +442,10 @@
|
||||
data.statistics = this.#statistics;
|
||||
}
|
||||
|
||||
if (!data.reasoning && !data.text && !data.tool) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
@@ -512,6 +517,7 @@
|
||||
this.#tool = tool;
|
||||
|
||||
this.#render("tool");
|
||||
this.#save();
|
||||
}
|
||||
|
||||
addReasoning(chunk) {
|
||||
@@ -687,6 +693,9 @@
|
||||
$version.innerHTML = `<a href="https://github.com/coalaura/whiskr" target="_blank">whiskr</a> <a href="https://github.com/coalaura/whiskr/releases/tag/${data.version}" target="_blank">${data.version}</a>`;
|
||||
}
|
||||
|
||||
// update search availability
|
||||
searchAvailable = data.search;
|
||||
|
||||
// render models
|
||||
$model.innerHTML = "";
|
||||
|
||||
@@ -805,12 +814,12 @@
|
||||
}
|
||||
|
||||
const hasJson = tags.includes("json"),
|
||||
hasTools = tags.includes("tools");
|
||||
hasSearch = searchAvailable && tags.includes("tools");
|
||||
|
||||
$json.classList.toggle("none", !hasJson);
|
||||
$search.classList.toggle("none", !hasTools);
|
||||
$search.classList.toggle("none", !hasSearch);
|
||||
|
||||
$search.parentNode.classList.toggle("none", !hasJson && !hasTools);
|
||||
$search.parentNode.classList.toggle("none", !hasJson && !hasSearch);
|
||||
});
|
||||
|
||||
$prompt.addEventListener("change", () => {
|
||||
@@ -952,7 +961,7 @@
|
||||
search: searchTool,
|
||||
messages: messages
|
||||
.map((message) => message.getData())
|
||||
.filter((data) => data?.text),
|
||||
.filter(Boolean),
|
||||
};
|
||||
|
||||
let message, generationID;
|
||||
@@ -1012,6 +1021,10 @@
|
||||
}
|
||||
|
||||
switch (chunk.type) {
|
||||
case "end":
|
||||
finish();
|
||||
|
||||
break;
|
||||
case "id":
|
||||
generationID = chunk.text;
|
||||
|
||||
@@ -1020,7 +1033,7 @@
|
||||
message.setState("tooling");
|
||||
message.setTool(chunk.text);
|
||||
|
||||
if (chunk.text.result) {
|
||||
if (chunk.text.done) {
|
||||
finish();
|
||||
}
|
||||
|
||||
|
@@ -37,6 +37,8 @@ func NewStream(w http.ResponseWriter) (*Stream, error) {
|
||||
}
|
||||
|
||||
func (s *Stream) Send(ch Chunk) error {
|
||||
debugIf(ch.Type == "error", "error: %v", ch.Text)
|
||||
|
||||
if err := s.en.Encode(ch); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -60,7 +62,7 @@ func ReasoningChunk(text string) Chunk {
|
||||
func TextChunk(text string) Chunk {
|
||||
return Chunk{
|
||||
Type: "text",
|
||||
Text: text,
|
||||
Text: CleanChunk(text),
|
||||
}
|
||||
}
|
||||
|
||||
|
Reference in New Issue
Block a user