-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathProgram.fs
More file actions
462 lines (379 loc) · 20.5 KB
/
Program.fs
File metadata and controls
462 lines (379 loc) · 20.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
open System
open System.Threading
open FSharp.Control
open System.Net.Http
open System.Net.NetworkInformation
open System.Text
open System.Text.Json
open Domain
open System.Diagnostics
open System.Collections.Generic
open System.IO
open System.Threading.Tasks
open Spectre
open Spectre.Console
open FSharp.Json
open Anthropic
open ModelContextProtocol.Client
open ModelContextProtocol.Protocol.Transport
open Microsoft.Extensions.AI
// Define types for API request and response
type ClaudeContentBlock = { Type: string; Text: string }
type ClaudeResponse =
{ Id: string
Content: ClaudeContentBlock[]
Model: string
StopReason: string
StopSequence: string option
Usage:
{| InputTokens: int
OutputTokens: int |} }
type ClaudeRequest =
{ Model: string
[<JsonField("max_tokens")>]
MaxTokens: int
Messages: ChatMessage[]
System: string
Stream: bool }
module UI =
let display (state: State) =
let printBold (text: string) = AnsiConsole.Markup($"[bold]{text}[/]")
match state.Message with
| You _ ->
printBold ">>>"
printf " "
| _ -> ()
state
let await f = f |> Async.RunSynchronously
let withNewChat (msg: Domain.Message) (convo: Conversation) =
match msg with
| Quit -> convo
| You msg -> [ yield! convo; You msg ]
| Jarvis msg -> [ yield! convo; Jarvis msg ]
let ask llm state : ChatContent =
let (payload, httpRequest) =
match llm with
| Ollama _ ->
let model = "jarvis"
let payload = LLM.createPayload state (Ollama model)
let request = Ollama.httpRequest payload
(payload, request)
| Claude _ ->
let model = "claude-sonnet-4-20250514"
let payload = LLM.createPayload state (Claude model)
let request = Claude.httpRequest payload
(payload, request)
let parseHandler =
match llm with
| Ollama _ -> Ollama.parse
| Claude _ -> Claude.parse
let jsonOptions = JsonSerializerOptions(PropertyNameCaseInsensitive = true)
LLM.makeRequest httpRequest parseHandler payload
|> (fun stream ->
async {
let startInfo = ProcessStartInfo()
startInfo.FileName <- $"/Users/amirpanahi/Documents/projects/jarvis/prettified-output/main"
startInfo.UseShellExecute <- false
startInfo.RedirectStandardInput <- true
use proc = Process.Start(startInfo)
use stdin = proc.StandardInput
let! res =
stream
|> AsyncSeq.foldAsync<ParseContext, Event>
(fun acc content ->
async {
// printfn "%A" acc
// printfn ""
// if acc.RestartRenderer then
// proc.Start() |> ignore
match content with
| ReceivedResponse res ->
do! stdin.WriteAsync(content.Serialize None (Some res)) |> Async.AwaitTask
do! stdin.FlushAsync() |> Async.AwaitTask
return
{ acc with
Response = ChatContent.Text(acc.Response.Serialize() + res) }
| RequiresTool tool ->
do! stdin.WriteAsync(content.Serialize (Some tool.name) None) |> Async.AwaitTask
do! stdin.FlushAsync() |> Async.AwaitTask
return { acc with Tool = Some tool, "" }
| ConstructingToolSchema partial ->
let toolName =
fst acc.Tool |> Option.map (fun x -> x.name) |> Option.defaultValue "Unknown"
do! stdin.WriteAsync(content.Serialize (Some toolName) None) |> Async.AwaitTask
do! stdin.FlushAsync() |> Async.AwaitTask
// Safely append schema parts - trim to avoid malformed JSON
let cleanPartial = partial.Trim()
let toolUpdated = (fst acc.Tool, snd acc.Tool + cleanPartial)
return { acc with Tool = toolUpdated }
| CallTool ->
let latestTextOutput =
acc.Response
|> function
| Text t -> t
| _ -> ""
let (_tool, schema) = acc.Tool
match _tool with
| Some tool ->
let client =
state.McpServerTools
|> Array.collect (fun x -> x |> Array.filter (fun y -> y.Name = tool.name))
|> Array.tryHead
match client with
| Some x ->
let schemaProcessed = if String.IsNullOrEmpty schema then "{}" else schema
// Try to correctly parse the schema JSON
let! outcome =
async {
try
let schemaDict =
schemaProcessed
|> JsonSerializer.Deserialize<Dictionary<string, obj>>
let functionArgs = AIFunctionArguments(schemaDict)
let! result =
x.InvokeAsync(functionArgs).AsTask() |> Async.AwaitTask
// Direct access to response content without double serialization
let output = result
// Extract just the text content safely
let outcome =
try
let output =
result
|> JsonSerializer.Serialize
|> JsonSerializer.Deserialize<
ModelContextProtocol.Protocol.Types.CallToolResponse
>
Some (output.Content.Item 0).Text
with _ ->
// Fallback if content structure is unexpected
printfn "Error processing tool response"
None
return outcome
with ex ->
// Handle schema parsing errors
printfn "Error parsing tool schema: %s" ex.Message
return None
}
let toolResponseUser =
{ ``type`` = tool.``type``
tool_use_id = tool.id
content = outcome }
let printToolCall =
Event.ReceivedResponse $"\n> [{tool.name}] called w/ ```{schemaProcessed}```"
do!
stdin.WriteAsync(
printToolCall.Serialize
None
None
)
|> Async.AwaitTask
do!
stdin.WriteAsync(
content.Serialize
(Some tool.name)
None
)
|> Async.AwaitTask
do! stdin.FlushAsync() |> Async.AwaitTask
return
{ acc with
Tool = None, ""
Response =
JarvisToolResponse.Create schema latestTextOutput tool
|> (fun x -> (Some x, Some toolResponseUser))
|> ChatContent.Tool }
| None -> return acc
| None -> return acc
})
{ Response = ChatContent.Text ""
Tool = None, "" }
stdin.Close()
proc.WaitForExit()
return res.Response
})
|> await
let withNewestPrompt state = List.last state.Conversation
let rec chat (state: State) (llm: LLM) =
match state.Message with
| You prompt ->
state |> UI.display |> ignore
match prompt with
| Implicit response ->
let newState =
{ state with
Message = "" |> ChatContent.Text |> Explicit |> Jarvis
Conversation = withNewChat (You(Explicit response)) state.Conversation } //end
chat newState llm
| Explicit response ->
let input = System.Console.ReadLine()
let newState =
match input with
| "/exit"
| "/quit" -> { state with Message = Quit }
// | "/retain" ->
// printfn "Generating summary of chat history..."
// try
// let content = state.Conversation |> Json.serialize
// if String.IsNullOrEmpty content then
// printfn "No conversation history to summarize."
// state
// else
// // Create direct API request to Anthropic
// let client = new HttpClient()
// // Prepare the payload
// let apiKey = Environment.GetEnvironmentVariable("ANTHROPIC_API_KEY")
// let systemPrompt =
// "Write 5 concise bullet points rich in information that describe any design decisions, insights uncovered, errors and issues encountered from the chat history that would be useful for future reference. Things like: 'I need to bare in mind X' or 'Key points to consider are X' or 'As a result of X then Y'. Notes that will help me in the future keep on top of how things morphozises over time"
// // Create the request payload
// let payload =
// { Model = "claude-3-5-sonnet-20241022"
// MaxTokens = 1024
// Messages = [| { role = "user"; content = content } |]
// System = systemPrompt
// Stream = false }
// let config = JsonConfig.create (jsonFieldNaming = Json.snakeCase)
// let payloadJson = Json.serializeEx config payload
// let content = new StringContent(payloadJson, Encoding.UTF8, "application/json")
// // Create request
// let request = new HttpRequestMessage()
// request.Method <- HttpMethod.Post
// request.RequestUri <- Uri("https://api.anthropic.com/v1/messages")
// request.Content <- content
// request.Headers.Add("x-api-key", apiKey)
// request.Headers.Add("anthropic-version", "2023-06-01")
// // Execute request
// let exec =
// async {
// try
// // Print request payload for debugging
// printfn "Debug - Request payload: %s" payloadJson
// use! response = client.SendAsync(request) |> Async.AwaitTask
// // Get response body even if status code indicates failure
// let! responseBody = response.Content.ReadAsStringAsync() |> Async.AwaitTask
// printfn
// "Debug - Status code: %d %s"
// (int response.StatusCode)
// (response.StatusCode.ToString())
// printfn "Debug - Raw response: %s" responseBody
// // Check status code after logging response
// response.EnsureSuccessStatusCode() |> ignore
// // Parse the response
// let deserializeOptions =
// JsonSerializerOptions(PropertyNameCaseInsensitive = true)
// let result =
// JsonSerializer.Deserialize<ClaudeResponse>(responseBody, deserializeOptions)
// return Ok result
// with ex ->
// return Error ex
// }
// match exec |> await with
// | Ok result ->
// try
// let textContent = result.Content |> Array.tryFind (fun c -> c.Type = "text")
// match textContent with
// | Some content ->
// let summary = content.Text
// printfn "\nSummary of chat history:"
// printfn "%s" summary
// // Append to CLAUDE.md file
// try
// let contentToWrite = sprintf "\n(%s)\n" summary
// let claudeFilePath =
// Path.Combine(Directory.GetCurrentDirectory(), "CLAUDE.md")
// // Check if file exists, create if not
// if not (File.Exists(claudeFilePath)) then
// File.WriteAllText(claudeFilePath, "# Jarvis Chat Summaries\n")
// // Append the content
// File.AppendAllText(claudeFilePath, contentToWrite)
// printfn "Summary appended to CLAUDE.md"
// with ex ->
// printfn "Error writing to file: %s" ex.Message
// | None -> printfn "No text content found in response."
// with ex ->
// printfn "Error parsing response: %s" ex.Message
// | Error ex -> printfn "API call failed: %s" ex.Message
// state
// with ex ->
// printfn "Error generating summary: %s" ex.Message
// state
| "/end" ->
{ state with
Message = "" |> ChatContent.Text |> Explicit |> Jarvis
Conversation = withNewChat (You(Explicit response)) state.Conversation } //end
| str ->
//append text to accumulated message
let accumulatedMsg = $"{response.Serialize()}\n{str}"
{ state with
Message = You(Explicit(ChatContent.Text accumulatedMsg)) }
chat newState llm
| Jarvis said ->
state |> UI.display |> ignore
//ask for jarvis input -> ollama rest api call
let response = state |> ask llm
// printfn "%A" state.Conversation
chat
(match response with
| Text t ->
{ state with
Conversation = withNewChat (t |> ChatContent.Text |> Explicit |> Jarvis) state.Conversation
Message = You(Explicit(ChatContent.Text "")) }
| Tool(jarvis_tr, user_tr) ->
let newConvo =
state.Conversation
|> withNewChat (Jarvis(Implicit(ChatContent.Tool(jarvis_tr, user_tr)))) //tool invoked
|> withNewChat (You(Implicit(ChatContent.Tool(jarvis_tr, user_tr)))) //tool answer
{ state with
Conversation = newConvo
Message = "" |> ChatContent.Text |> Explicit |> Jarvis }) //request jarvis again after tool use done to generate outcome
llm
| Quit -> ()
let main argv =
task {
let isInternetAvailable () =
try
use ping = new System.Net.NetworkInformation.Ping()
let reply = ping.Send("8.8.8.8") // Ping Google DNS
reply.Status = IPStatus.Success
with _ ->
false
let! mcpServers =
task {
let! config = MCP.readConfig $"{Directory.GetCurrentDirectory()}/mcp-servers.json"
match config with
| Some serverList ->
let! mcpClients = MCP.createClients serverList
let! tools = MCP.listTools mcpClients
return Some tools
| None -> return None
}
let initially =
{ Message = You(Explicit(ChatContent.Text ""))
Conversation = List.Empty
WithLogging = false
McpServerTools =
mcpServers
|> Option.map (fun x -> x |> Array.map snd)
|> Option.defaultValue [||] }
match argv |> Array.toList with
| dll :: rest ->
match rest with
| [ "tools" ] -> MCP.display mcpServers true
| [ "-l"] ->
let llm =
match isInternetAvailable () with
| true -> Claude ""
| false -> Ollama "" // Ollama can be used offline
MCP.display mcpServers false
chat {initially with WithLogging = true } llm
| _ ->
let llm =
match isInternetAvailable () with
| true -> Claude ""
| false -> Ollama "" // Ollama can be used offline
MCP.display mcpServers false
chat initially llm
| _ -> ()
return 0
}
let argv = System.Environment.GetCommandLineArgs()
main argv |> Async.AwaitTask |> Async.RunSynchronously |> ignore