openai/symphony

Public

mirrored from https://github.com/openai/symphonyAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
fecbc92a1590dd46c4bcb9df31c76f4824e8caf1

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

elixir/lib/symphony_elixir/codex/dynamic_tool.ex

209lines · modecode

1defmodule SymphonyElixir.Codex.DynamicTool do
2 @moduledoc """
3 Executes client-side tool calls requested by Codex app-server turns.
4 """
5
6 alias SymphonyElixir.Linear.Client
7
8 @linear_graphql_tool "linear_graphql"
9 @linear_graphql_description """
10 Execute a raw GraphQL query or mutation against Linear using Symphony's configured auth.
11 """
12 @linear_graphql_input_schema %{
13 "type" => "object",
14 "additionalProperties" => false,
15 "required" => ["query"],
16 "properties" => %{
17 "query" => %{
18 "type" => "string",
19 "description" => "GraphQL query or mutation document to execute against Linear."
20 },
21 "variables" => %{
22 "type" => ["object", "null"],
23 "description" => "Optional GraphQL variables object.",
24 "additionalProperties" => true
25 }
26 }
27 }
28
29 @spec execute(String.t() | nil, term(), keyword()) :: map()
30 def execute(tool, arguments, opts \\ []) do
31 case tool do
32 @linear_graphql_tool ->
33 execute_linear_graphql(arguments, opts)
34
35 other ->
36 failure_response(%{
37 "error" => %{
38 "message" => "Unsupported dynamic tool: #{inspect(other)}.",
39 "supportedTools" => supported_tool_names()
40 }
41 })
42 end
43 end
44
45 @spec tool_specs() :: [map()]
46 def tool_specs do
47 [
48 %{
49 "name" => @linear_graphql_tool,
50 "description" => @linear_graphql_description,
51 "inputSchema" => @linear_graphql_input_schema
52 }
53 ]
54 end
55
56 defp execute_linear_graphql(arguments, opts) do
57 linear_client = Keyword.get(opts, :linear_client, &Client.graphql/3)
58
59 with {:ok, query, variables} <- normalize_linear_graphql_arguments(arguments),
60 {:ok, response} <- linear_client.(query, variables, []) do
61 graphql_response(response)
62 else
63 {:error, reason} ->
64 failure_response(tool_error_payload(reason))
65 end
66 end
67
68 defp normalize_linear_graphql_arguments(arguments) when is_binary(arguments) do
69 case String.trim(arguments) do
70 "" -> {:error, :missing_query}
71 query -> {:ok, query, %{}}
72 end
73 end
74
75 defp normalize_linear_graphql_arguments(arguments) when is_map(arguments) do
76 case normalize_query(arguments) do
77 {:ok, query} ->
78 case normalize_variables(arguments) do
79 {:ok, variables} ->
80 {:ok, query, variables}
81
82 {:error, reason} ->
83 {:error, reason}
84 end
85
86 {:error, reason} ->
87 {:error, reason}
88 end
89 end
90
91 defp normalize_linear_graphql_arguments(_arguments), do: {:error, :invalid_arguments}
92
93 defp normalize_query(arguments) do
94 case Map.get(arguments, "query") || Map.get(arguments, :query) do
95 query when is_binary(query) ->
96 case String.trim(query) do
97 "" -> {:error, :missing_query}
98 trimmed -> {:ok, trimmed}
99 end
100
101 _ ->
102 {:error, :missing_query}
103 end
104 end
105
106 defp normalize_variables(arguments) do
107 case Map.get(arguments, "variables") || Map.get(arguments, :variables) || %{} do
108 variables when is_map(variables) -> {:ok, variables}
109 _ -> {:error, :invalid_variables}
110 end
111 end
112
113 defp graphql_response(response) do
114 success =
115 case response do
116 %{"errors" => errors} when is_list(errors) and errors != [] -> false
117 %{errors: errors} when is_list(errors) and errors != [] -> false
118 _ -> true
119 end
120
121 dynamic_tool_response(success, encode_payload(response))
122 end
123
124 defp failure_response(payload) do
125 dynamic_tool_response(false, encode_payload(payload))
126 end
127
128 defp dynamic_tool_response(success, output) when is_boolean(success) and is_binary(output) do
129 %{
130 "success" => success,
131 "output" => output,
132 "contentItems" => [
133 %{
134 "type" => "inputText",
135 "text" => output
136 }
137 ]
138 }
139 end
140
141 defp encode_payload(payload) when is_map(payload) or is_list(payload) do
142 Jason.encode!(payload, pretty: true)
143 end
144
145 defp encode_payload(payload), do: inspect(payload)
146
147 defp tool_error_payload(:missing_query) do
148 %{
149 "error" => %{
150 "message" => "`linear_graphql` requires a non-empty `query` string."
151 }
152 }
153 end
154
155 defp tool_error_payload(:invalid_arguments) do
156 %{
157 "error" => %{
158 "message" => "`linear_graphql` expects either a GraphQL query string or an object with `query` and optional `variables`."
159 }
160 }
161 end
162
163 defp tool_error_payload(:invalid_variables) do
164 %{
165 "error" => %{
166 "message" => "`linear_graphql.variables` must be a JSON object when provided."
167 }
168 }
169 end
170
171 defp tool_error_payload(:missing_linear_api_token) do
172 %{
173 "error" => %{
174 "message" => "Symphony is missing Linear auth. Set `linear.api_key` in `WORKFLOW.md` or export `LINEAR_API_KEY`."
175 }
176 }
177 end
178
179 defp tool_error_payload({:linear_api_status, status}) do
180 %{
181 "error" => %{
182 "message" => "Linear GraphQL request failed with HTTP #{status}.",
183 "status" => status
184 }
185 }
186 end
187
188 defp tool_error_payload({:linear_api_request, reason}) do
189 %{
190 "error" => %{
191 "message" => "Linear GraphQL request failed before receiving a successful response.",
192 "reason" => inspect(reason)
193 }
194 }
195 end
196
197 defp tool_error_payload(reason) do
198 %{
199 "error" => %{
200 "message" => "Linear GraphQL tool execution failed.",
201 "reason" => inspect(reason)
202 }
203 }
204 end
205
206 defp supported_tool_names do
207 Enum.map(tool_specs(), & &1["name"])
208 end
209end
210