llm_test.go 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. package main
  2. import (
  3. "encoding/json"
  4. "testing"
  5. "github.com/sashabaranov/go-openai"
  6. )
  7. // TestLLM_ConvertMCPToolsToOpenAI tests the MCP to OpenAI tool conversion
  8. func TestLLM_ConvertMCPToolsToOpenAI(t *testing.T) {
  9. tests := []struct {
  10. name string
  11. mcpTools []Tool
  12. wantLen int
  13. }{
  14. {
  15. name: "EmptyTools",
  16. mcpTools: []Tool{},
  17. wantLen: 0,
  18. },
  19. {
  20. name: "SingleTool",
  21. mcpTools: []Tool{
  22. {
  23. Name: "introspect",
  24. Description: "Discover the GraphQL schema",
  25. InputSchema: InputSchema{
  26. Type: "object",
  27. Properties: map[string]Property{
  28. "typeName": {Type: "string", Description: "The type to introspect"},
  29. },
  30. Required: []string{},
  31. AdditionalProperties: false,
  32. },
  33. },
  34. },
  35. wantLen: 1,
  36. },
  37. {
  38. name: "MultipleTools",
  39. mcpTools: []Tool{
  40. {
  41. Name: "query",
  42. Description: "Execute a GraphQL query",
  43. InputSchema: InputSchema{
  44. Type: "object",
  45. Properties: map[string]Property{
  46. "query": {Type: "string", Description: "The GraphQL query"},
  47. },
  48. Required: []string{"query"},
  49. AdditionalProperties: false,
  50. },
  51. },
  52. {
  53. Name: "mutate",
  54. Description: "Execute a GraphQL mutation",
  55. InputSchema: InputSchema{
  56. Type: "object",
  57. Properties: map[string]Property{
  58. "mutation": {Type: "string", Description: "The GraphQL mutation"},
  59. },
  60. Required: []string{"mutation"},
  61. AdditionalProperties: false,
  62. },
  63. },
  64. },
  65. wantLen: 2,
  66. },
  67. }
  68. for _, tt := range tests {
  69. t.Run(tt.name, func(t *testing.T) {
  70. tools := ConvertMCPToolsToOpenAI(tt.mcpTools)
  71. if len(tools) != tt.wantLen {
  72. t.Errorf("Expected %d tools, got %d", tt.wantLen, len(tools))
  73. }
  74. // Verify tool conversion details
  75. for i, tool := range tools {
  76. if tool.Type != openai.ToolTypeFunction {
  77. t.Errorf("Tool %d: Expected type %s, got %s", i, openai.ToolTypeFunction, tool.Type)
  78. }
  79. if tool.Function.Name != tt.mcpTools[i].Name {
  80. t.Errorf("Tool %d: Expected name %s, got %s", i, tt.mcpTools[i].Name, tool.Function.Name)
  81. }
  82. if tool.Function.Description != tt.mcpTools[i].Description {
  83. t.Errorf("Tool %d: Expected description %s, got %s", i, tt.mcpTools[i].Description, tool.Function.Description)
  84. }
  85. }
  86. })
  87. }
  88. }
  89. // TestLLM_ConvertMCPToolsToOpenAI_ObjectProperties tests that object-type properties
  90. // get additionalProperties: true to allow arbitrary key-value pairs
  91. func TestLLM_ConvertMCPToolsToOpenAI_ObjectProperties(t *testing.T) {
  92. mcpTools := []Tool{
  93. {
  94. Name: "query",
  95. Description: "Execute a GraphQL query",
  96. InputSchema: InputSchema{
  97. Type: "object",
  98. Properties: map[string]Property{
  99. "query": {
  100. Type: "string",
  101. Description: "The GraphQL query string",
  102. },
  103. "variables": {
  104. Type: "object",
  105. Description: "Optional query variables as key-value pairs",
  106. },
  107. },
  108. Required: []string{"query"},
  109. AdditionalProperties: false,
  110. },
  111. },
  112. }
  113. tools := ConvertMCPToolsToOpenAI(mcpTools)
  114. if len(tools) != 1 {
  115. t.Fatalf("Expected 1 tool, got %d", len(tools))
  116. }
  117. // Check that parameters don't have additionalProperties at top level
  118. params := tools[0].Function.Parameters.(map[string]interface{})
  119. if _, hasAdditionalProps := params["additionalProperties"]; hasAdditionalProps {
  120. t.Error("Top-level parameters should NOT have additionalProperties field")
  121. }
  122. // Check that the variables property has additionalProperties: true
  123. props := params["properties"].(map[string]interface{})
  124. variablesProp, ok := props["variables"].(map[string]interface{})
  125. if !ok {
  126. t.Fatal("variables property not found")
  127. }
  128. if additionalProps, ok := variablesProp["additionalProperties"]; !ok {
  129. t.Error("Object property 'variables' should have additionalProperties field")
  130. } else if additionalProps != true {
  131. t.Errorf("Object property 'variables' additionalProperties should be true, got %v", additionalProps)
  132. }
  133. // Check that string property does NOT have additionalProperties
  134. queryProp, ok := props["query"].(map[string]interface{})
  135. if !ok {
  136. t.Fatal("query property not found")
  137. }
  138. if _, hasAdditionalProps := queryProp["additionalProperties"]; hasAdditionalProps {
  139. t.Error("String property 'query' should NOT have additionalProperties field")
  140. }
  141. }
  142. // TestLLM_ParseToolCall tests parsing tool calls from LLM responses
  143. func TestLLM_ParseToolCall(t *testing.T) {
  144. tests := []struct {
  145. name string
  146. toolCall openai.ToolCall
  147. wantName string
  148. wantArgs map[string]interface{}
  149. wantErr bool
  150. }{
  151. {
  152. name: "ValidToolCall",
  153. toolCall: openai.ToolCall{
  154. ID: "call-123",
  155. Function: openai.FunctionCall{
  156. Name: "query",
  157. Arguments: `{"query": "{ users { email } }"}`,
  158. },
  159. },
  160. wantName: "query",
  161. wantArgs: map[string]interface{}{
  162. "query": "{ users { email } }",
  163. },
  164. wantErr: false,
  165. },
  166. {
  167. name: "EmptyArguments",
  168. toolCall: openai.ToolCall{
  169. ID: "call-456",
  170. Function: openai.FunctionCall{
  171. Name: "introspect",
  172. Arguments: `{}`,
  173. },
  174. },
  175. wantName: "introspect",
  176. wantArgs: map[string]interface{}{},
  177. wantErr: false,
  178. },
  179. {
  180. name: "InvalidJSON",
  181. toolCall: openai.ToolCall{
  182. ID: "call-789",
  183. Function: openai.FunctionCall{
  184. Name: "mutate",
  185. Arguments: `invalid json`,
  186. },
  187. },
  188. wantName: "mutate",
  189. wantArgs: nil,
  190. wantErr: true,
  191. },
  192. {
  193. name: "NestedArguments",
  194. toolCall: openai.ToolCall{
  195. ID: "call-abc",
  196. Function: openai.FunctionCall{
  197. Name: "createTask",
  198. Arguments: `{"title": "Test Task", "priority": "high", "assigneeId": "user-123"}`,
  199. },
  200. },
  201. wantName: "createTask",
  202. wantArgs: map[string]interface{}{
  203. "title": "Test Task",
  204. "priority": "high",
  205. "assigneeId": "user-123",
  206. },
  207. wantErr: false,
  208. },
  209. }
  210. for _, tt := range tests {
  211. t.Run(tt.name, func(t *testing.T) {
  212. name, args, err := ParseToolCall(tt.toolCall)
  213. if (err != nil) != tt.wantErr {
  214. t.Errorf("ParseToolCall() error = %v, wantErr %v", err, tt.wantErr)
  215. return
  216. }
  217. if name != tt.wantName {
  218. t.Errorf("ParseToolCall() name = %v, want %v", name, tt.wantName)
  219. }
  220. if !tt.wantErr && args != nil {
  221. // Compare args
  222. argsJSON, _ := json.Marshal(args)
  223. wantJSON, _ := json.Marshal(tt.wantArgs)
  224. if string(argsJSON) != string(wantJSON) {
  225. t.Errorf("ParseToolCall() args = %v, want %v", args, tt.wantArgs)
  226. }
  227. }
  228. })
  229. }
  230. }
  231. // TestLLM_ToolConversionSnapshot tests tool conversion with snapshot
  232. func TestLLM_ToolConversionSnapshot(t *testing.T) {
  233. mcpTools := []Tool{
  234. {
  235. Name: "introspect",
  236. Description: "Discover the GraphQL schema structure",
  237. InputSchema: InputSchema{
  238. Type: "object",
  239. Properties: map[string]Property{
  240. "typeName": {
  241. Type: "string",
  242. Description: "Optional type name to introspect",
  243. },
  244. },
  245. Required: []string{},
  246. AdditionalProperties: false,
  247. },
  248. },
  249. {
  250. Name: "query",
  251. Description: "Execute a GraphQL query",
  252. InputSchema: InputSchema{
  253. Type: "object",
  254. Properties: map[string]Property{
  255. "query": {
  256. Type: "string",
  257. Description: "The GraphQL query string",
  258. },
  259. },
  260. Required: []string{"query"},
  261. AdditionalProperties: false,
  262. },
  263. },
  264. {
  265. Name: "mutate",
  266. Description: "Execute a GraphQL mutation",
  267. InputSchema: InputSchema{
  268. Type: "object",
  269. Properties: map[string]Property{
  270. "mutation": {
  271. Type: "string",
  272. Description: "The GraphQL mutation string",
  273. },
  274. },
  275. Required: []string{"mutation"},
  276. AdditionalProperties: false,
  277. },
  278. },
  279. }
  280. openaiTools := ConvertMCPToolsToOpenAI(mcpTools)
  281. testSnapshotResult(t, "converted_tools", openaiTools)
  282. }