| | package parser
|
| |
|
| | import (
|
| | "encoding/json"
|
| | "opus-api/internal/types"
|
| | "regexp"
|
| | "sort"
|
| | "strings"
|
| | )
|
| |
|
| | type TagPosition struct {
|
| | Type string
|
| | Index int
|
| | Name string
|
| | }
|
| |
|
| | type ParseResult struct {
|
| | ToolCalls []types.ParsedToolCall
|
| | RemainingText string
|
| | }
|
| |
|
| |
|
| | var stringOnlyParams = map[string]bool{
|
| | "taskId": true,
|
| | }
|
| |
|
| |
|
| | func shouldKeepAsString(paramName string) bool {
|
| | return stringOnlyParams[paramName]
|
| | }
|
| |
|
| | type NextToolCallResult struct {
|
| | ToolCall *types.ParsedToolCall
|
| | EndPosition int
|
| | Found bool
|
| | }
|
| |
|
| | func ParseNextToolCall(text string) NextToolCallResult {
|
| | invokeStart := strings.Index(text, "<invoke name=\"")
|
| | if invokeStart == -1 {
|
| | return NextToolCallResult{Found: false}
|
| | }
|
| |
|
| | depth := 0
|
| | pos := invokeStart
|
| | for pos < len(text) {
|
| | nextOpen := strings.Index(text[pos:], "<invoke")
|
| | nextClose := strings.Index(text[pos:], "</invoke>")
|
| |
|
| | if nextOpen == -1 && nextClose == -1 {
|
| | break
|
| | }
|
| |
|
| | if nextOpen != -1 && (nextClose == -1 || nextOpen < nextClose) {
|
| | depth++
|
| | pos = pos + nextOpen + 7
|
| | } else {
|
| | depth--
|
| | if depth == 0 {
|
| | endPos := pos + nextClose + 9
|
| | invokeBlock := text[invokeStart:endPos]
|
| | toolCalls := parseInvokeTags(invokeBlock)
|
| | if len(toolCalls) > 0 {
|
| | return NextToolCallResult{
|
| | ToolCall: &toolCalls[0],
|
| | EndPosition: endPos,
|
| | Found: true,
|
| | }
|
| | }
|
| | return NextToolCallResult{Found: false}
|
| | }
|
| | pos = pos + nextClose + 9
|
| | }
|
| | }
|
| |
|
| | return NextToolCallResult{Found: false}
|
| | }
|
| |
|
| | func ParseToolCalls(text string) ParseResult {
|
| | blockInfo := FindToolCallBlockAtEnd(text)
|
| | if blockInfo == nil {
|
| | return ParseResult{
|
| | ToolCalls: []types.ParsedToolCall{},
|
| | RemainingText: text,
|
| | }
|
| | }
|
| | remainingText := strings.TrimSpace(text[:blockInfo.StartIndex])
|
| | toolCallBlock := text[blockInfo.StartIndex:]
|
| | openTag := "<" + blockInfo.TagType + ">"
|
| | closeTag := "</" + blockInfo.TagType + ">"
|
| | openTagIndex := strings.Index(toolCallBlock, openTag)
|
| | closeTagIndex := strings.LastIndex(toolCallBlock, closeTag)
|
| | var innerContent string
|
| | if closeTagIndex != -1 && closeTagIndex > openTagIndex {
|
| | innerContent = toolCallBlock[openTagIndex+len(openTag) : closeTagIndex]
|
| | } else {
|
| | innerContent = toolCallBlock[openTagIndex+len(openTag):]
|
| | }
|
| | toolCalls := parseInvokeTags(innerContent)
|
| | return ParseResult{
|
| | ToolCalls: toolCalls,
|
| | RemainingText: remainingText,
|
| | }
|
| | }
|
| |
|
| | func parseInvokeTags(innerContent string) []types.ParsedToolCall {
|
| | var toolCalls []types.ParsedToolCall
|
| | invokeStartRegex := regexp.MustCompile(`<invoke name="([^"]+)">`)
|
| | invokeEndRegex := regexp.MustCompile(`</invoke>`)
|
| | var positions []TagPosition
|
| | for _, match := range invokeStartRegex.FindAllStringSubmatchIndex(innerContent, -1) {
|
| | positions = append(positions, TagPosition{
|
| | Type: "start",
|
| | Index: match[0],
|
| | Name: innerContent[match[2]:match[3]],
|
| | })
|
| | }
|
| | for _, match := range invokeEndRegex.FindAllStringIndex(innerContent, -1) {
|
| | positions = append(positions, TagPosition{
|
| | Type: "end",
|
| | Index: match[0],
|
| | })
|
| | }
|
| | sort.Slice(positions, func(i, j int) bool {
|
| | return positions[i].Index < positions[j].Index
|
| | })
|
| | depth := 0
|
| | var currentInvoke *struct {
|
| | Name string
|
| | StartIndex int
|
| | }
|
| | type InvokeBlock struct {
|
| | Name string
|
| | Start int
|
| | End int
|
| | }
|
| | var topLevelInvokes []InvokeBlock
|
| | for _, pos := range positions {
|
| | if pos.Type == "start" {
|
| | if depth == 0 {
|
| | currentInvoke = &struct {
|
| | Name string
|
| | StartIndex int
|
| | }{Name: pos.Name, StartIndex: pos.Index}
|
| | }
|
| | depth++
|
| | } else {
|
| | depth--
|
| | if depth == 0 && currentInvoke != nil {
|
| | topLevelInvokes = append(topLevelInvokes, InvokeBlock{
|
| | Name: currentInvoke.Name,
|
| | Start: currentInvoke.StartIndex,
|
| | End: pos.Index + 9,
|
| | })
|
| | currentInvoke = nil
|
| | }
|
| | }
|
| | }
|
| | if currentInvoke != nil && depth > 0 {
|
| | topLevelInvokes = append(topLevelInvokes, InvokeBlock{
|
| | Name: currentInvoke.Name,
|
| | Start: currentInvoke.StartIndex,
|
| | End: len(innerContent),
|
| | })
|
| | }
|
| | for _, invoke := range topLevelInvokes {
|
| | invokeContent := innerContent[invoke.Start:invoke.End]
|
| | input := make(map[string]interface{})
|
| | invokeTagEnd := strings.Index(invokeContent, ">") + 1
|
| | paramsContent := invokeContent[invokeTagEnd:]
|
| | paramStartRegex := regexp.MustCompile(`<parameter name="([^"]+)">`)
|
| | paramEndRegex := regexp.MustCompile(`</parameter>`)
|
| | var paramPositions []TagPosition
|
| | for _, match := range paramStartRegex.FindAllStringSubmatchIndex(paramsContent, -1) {
|
| | paramPositions = append(paramPositions, TagPosition{
|
| | Type: "start",
|
| | Index: match[0],
|
| | Name: paramsContent[match[2]:match[3]],
|
| | })
|
| | }
|
| | for _, match := range paramEndRegex.FindAllStringIndex(paramsContent, -1) {
|
| | paramPositions = append(paramPositions, TagPosition{
|
| | Type: "end",
|
| | Index: match[0],
|
| | })
|
| | }
|
| | sort.Slice(paramPositions, func(i, j int) bool {
|
| | return paramPositions[i].Index < paramPositions[j].Index
|
| | })
|
| | paramDepth := 0
|
| | var currentParam *struct {
|
| | Name string
|
| | StartIndex int
|
| | }
|
| | for _, pos := range paramPositions {
|
| | if pos.Type == "start" {
|
| | if paramDepth == 0 {
|
| | tagEndIndex := strings.Index(paramsContent[pos.Index:], ">") + pos.Index + 1
|
| | currentParam = &struct {
|
| | Name string
|
| | StartIndex int
|
| | }{Name: pos.Name, StartIndex: tagEndIndex}
|
| | }
|
| | paramDepth++
|
| | } else {
|
| | paramDepth--
|
| | if paramDepth == 0 && currentParam != nil {
|
| | value := paramsContent[currentParam.StartIndex:pos.Index]
|
| | trimmedValue := strings.TrimSpace(value)
|
| |
|
| |
|
| | if shouldKeepAsString(currentParam.Name) {
|
| | input[currentParam.Name] = trimmedValue
|
| | } else {
|
| |
|
| | var parsed interface{}
|
| | if err := json.Unmarshal([]byte(trimmedValue), &parsed); err == nil {
|
| |
|
| | input[currentParam.Name] = parsed
|
| | } else {
|
| |
|
| | input[currentParam.Name] = trimmedValue
|
| | }
|
| | }
|
| | currentParam = nil
|
| | }
|
| | }
|
| | }
|
| | if currentParam != nil && paramDepth > 0 {
|
| | value := paramsContent[currentParam.StartIndex:]
|
| | trimmedValue := strings.TrimSpace(value)
|
| |
|
| |
|
| | if shouldKeepAsString(currentParam.Name) {
|
| | input[currentParam.Name] = trimmedValue
|
| | } else {
|
| |
|
| | var parsed interface{}
|
| | if err := json.Unmarshal([]byte(trimmedValue), &parsed); err == nil {
|
| |
|
| | input[currentParam.Name] = parsed
|
| | } else {
|
| |
|
| | input[currentParam.Name] = trimmedValue
|
| | }
|
| | }
|
| | }
|
| | toolCalls = append(toolCalls, types.ParsedToolCall{Name: invoke.Name, Input: input})
|
| | }
|
| | return toolCalls
|
| | }
|
| |
|