Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,4 @@ fable_modules/
src/**/*.fs.js
src/Client/public/
test/e2e/Chrome
.playwright-mcp
12 changes: 12 additions & 0 deletions .mcp.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"mcpServers": {
"playwright": {
"type": "stdio",
"command": "npx",
"args": [
"@playwright/mcp@latest"
],
"env": {}
}
}
}
1 change: 1 addition & 0 deletions .serena/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/cache
68 changes: 68 additions & 0 deletions .serena/project.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
# * For C, use cpp
# * For JavaScript, use typescript
# Special requirements:
# * csharp: Requires the presence of a .sln file in the project folder.
language: bash

# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed) on 2025-04-07
ignored_paths: []

# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false


# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []

# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""

project_name: "SAFE-Chat"
76 changes: 37 additions & 39 deletions src/Server/AppGiraffe.fs
Original file line number Diff line number Diff line change
Expand Up @@ -34,27 +34,18 @@ open UserSessionFlow
let private (</>) a b = Path.Combine(a, b)

module Secrets =
let CookieSecretFile = "CHAT_DATA" </> "COOKIE_SECRET"
let OAuthConfigFile = "CHAT_DATA" </> "oauth.config"

let readCookieSecret () =
printfn "Reading configuration data from %s" System.Environment.CurrentDirectory
if not (File.Exists CookieSecretFile) then
let secret = System.Security.Cryptography.RandomNumberGenerator.GetBytes(32)
do (Path.GetDirectoryName CookieSecretFile) |> Directory.CreateDirectory |> ignore
File.WriteAllBytes (CookieSecretFile, secret)
File.ReadAllBytes(CookieSecretFile)

let oauthConfigData =
if not (File.Exists OAuthConfigFile) then
do (Path.GetDirectoryName OAuthConfigFile) |> Directory.CreateDirectory |> ignore
do Path.GetDirectoryName OAuthConfigFile |> Directory.CreateDirectory |> ignore
File.WriteAllText (OAuthConfigFile, """{
"google": {
"client_id": "<type in client id string>",
"client_secret": "<type in client secret>"
}
}""" )
ConfigurationBuilder().SetBasePath(System.Environment.CurrentDirectory).AddJsonFile(OAuthConfigFile).Build()
ConfigurationBuilder().SetBasePath(Environment.CurrentDirectory).AddJsonFile(OAuthConfigFile).Build()

type AppState = {
ActorSystem: ActorSystem option
Expand All @@ -69,8 +60,12 @@ let mutable private appServerState: AppState = { ActorSystem = None; UserStore =
// ---------------------------------

let startChatServer () = async {
// Create logger factory for initialization
let loggerFactory = LoggerFactory.Create (fun builder -> builder.AddConsole() |> ignore)
let logger = loggerFactory.CreateLogger("app-init")

try
printfn "Initializing actor system with in-memory persistence..."
logger.LogInformation "Initializing actor system with in-memory persistence..."

let configStr = """akka {
stdout-loglevel = WARNING
Expand Down Expand Up @@ -104,50 +99,50 @@ let startChatServer () = async {
}
}
}"""
let config = ConfigurationFactory.ParseString(configStr)
let config = ConfigurationFactory.ParseString configStr

printfn "Creating actor system..."
logger.LogInformation "Creating actor system..."
let actorSystem = ActorSystem.Create("chatapp", config)

printfn "Creating user store..."
logger.LogInformation "Creating user store..."
let userStore = UserStore.UserStore actorSystem

// Wait for actor system to initialize (shorter wait for in-memory)
printfn "Waiting for actor system initialization..."
logger.LogInformation "Waiting for actor system initialization..."
do! Async.Sleep(2000)

printfn "Starting chat server..."
logger.LogInformation("Starting chat server...")
let chatServer = ChatServer.startServer actorSystem

// Give the chat server time to start (shorter wait for in-memory)
printfn "Waiting for chat server to start..."
logger.LogInformation("Waiting for chat server to start...")
do! Async.Sleep(1000)

// Try to initialize channels with retry logic
let rec tryInitializeChannels retryCount =
async {
try
printfn "Creating diagnostic channel (attempt %d)..." (6 - retryCount)
do! Diag.createDiagChannel userStore.GetUser actorSystem chatServer (UserStore.UserIds.echo, "Demo", "Channel for testing purposes. Notice the bots are always ready to keep conversation.")
logger.LogInformation("Creating diagnostic channel (attempt {attempt})...", 6 - retryCount)
do! Diag.createDiagChannel logger userStore.GetUser actorSystem chatServer (UserStore.UserIds.echo, "Demo", "Channel for testing purposes. Notice the bots are always ready to keep conversation.")

printfn "Creating default channels (attempt %d)..." (6 - retryCount)
logger.LogInformation("Creating default channels (attempt {attempt})...", 6 - retryCount)
do! chatServer |> getOrCreateChannel "Test" "empty channel" (GroupChatChannel { autoRemove = false }) |> Async.Ignore
do! chatServer |> getOrCreateChannel "About" "interactive help" (OtherChannel <| AboutChannelActor.props UserStore.UserIds.system) |> Async.Ignore

printfn "Channels created successfully."
logger.LogInformation "Channels created successfully."
with
| ex when retryCount > 0 ->
printfn "Channel creation failed (attempt %d): %s. Retrying..." (6 - retryCount) ex.Message
do! Async.Sleep(3000)
logger.LogWarning("Channel creation failed (attempt {attempt}): {error}. Retrying...", 6 - retryCount, ex.Message)
do! Async.Sleep 3000
return! tryInitializeChannels (retryCount - 1)
| ex ->
printfn "Channel creation failed after all retries: %s" ex.Message
logger.LogError("Channel creation failed after all retries: {error}", ex.Message)
return failwith (sprintf "Failed to create channels: %s" ex.Message)
}

do! tryInitializeChannels 5

printfn "Chat server initialization completed successfully."
logger.LogInformation "Chat server initialization completed successfully."

appServerState <- {
ActorSystem = Some actorSystem
Expand All @@ -157,8 +152,8 @@ let startChatServer () = async {
return ()
with
| ex ->
printfn "Error during chat server initialization: %s" ex.Message
printfn "Stack trace: %s" ex.StackTrace
logger.LogError("Error during chat server initialization: {error}", ex.Message)
logger.LogError("Stack trace: {stackTrace}", ex.StackTrace)
return failwith (sprintf "Failed to initialize chat server: %s" ex.Message)
}

Expand All @@ -171,8 +166,8 @@ let getUserFromSession (ctx: HttpContext) = async {
| Some userStore ->
let userId = ctx.Session.GetString("userid")
if not (String.IsNullOrEmpty userId) then
let! result = userStore.GetUser (UserId userId)
return result |> Option.map (fun user -> RegisteredUser (UserId userId, user))
let! result = userStore.GetUser(UserId userId)
return result |> Option.map(fun user -> RegisteredUser(UserId userId, user))
else
return None
| None -> return None
Expand Down Expand Up @@ -234,7 +229,7 @@ let indexHandler : HttpFunc -> HttpFunc =
let! userOpt = getUserFromSession ctx
match userOpt with
| Some _ ->
let clientPublicPath = Path.Combine(Path.GetDirectoryName(System.Reflection.Assembly.GetExecutingAssembly().Location), "..", "..", "..", "..", "Client", "public", "index.html")
let clientPublicPath = Path.Combine(Path.GetDirectoryName(Reflection.Assembly.GetExecutingAssembly().Location), "..", "..", "..", "..", "Client", "public", "index.html")
let indexPath = Path.GetFullPath clientPublicPath
if File.Exists indexPath then
return! htmlFile indexPath next ctx
Expand All @@ -259,15 +254,15 @@ let logonPostHandler : HttpFunc -> HttpFunc =
match appServerState.UserStore with
| Some userStore ->
let! body = ctx.ReadBodyFromRequestAsync()
let nick = body.Substring(5) |> WebUtility.UrlDecode |> WebUtility.HtmlDecode
let nick = body.Substring 5 |> WebUtility.UrlDecode |> WebUtility.HtmlDecode
let user = {ChatUser.makeNew (Anonymous nick) nick with imageUrl = makeUserImageUrl "monsterid" nick}
let! registerResult = userStore.Register user
match registerResult with
| Ok (RegisteredUser(UserId userid, _)) ->
ctx.Session.SetString("userid", userid)
return! redirectTo false "/" next ctx
| Result.Error message ->
return! text (sprintf "Register failed because of `%s`" message) next ctx
return! text(sprintf "Register failed because of `%s`" message) next ctx
| None ->
return! text "Server not initialized" next ctx
}
Expand All @@ -292,10 +287,10 @@ let oauthCallbackHandler (provider: string) : HttpFunc -> HttpFunc =
let! result = ctx.AuthenticateAsync(provider)
if result.Succeeded then
let claims = result.Principal.Claims
let name = claims |> Seq.tryFind (fun c -> c.Type = ClaimTypes.Name) |> Option.map (fun c -> c.Value) |> Option.defaultValue "Unknown"
let id = claims |> Seq.tryFind (fun c -> c.Type = ClaimTypes.NameIdentifier) |> Option.map (fun c -> c.Value) |> Option.defaultValue (Guid.NewGuid().ToString())
let name = claims |> Seq.tryFind(fun c -> c.Type = ClaimTypes.Name) |> Option.map(fun c -> c.Value) |> Option.defaultValue "Unknown"
let id = claims |> Seq.tryFind(fun c -> c.Type = ClaimTypes.NameIdentifier) |> Option.map(fun c -> c.Value) |> Option.defaultValue(Guid.NewGuid().ToString())

let imageUrl = getUserImageUrl claims |> Option.orElseWith (fun () -> makeUserImageUrl "wavatar" name)
let imageUrl = getUserImageUrl claims |> Option.orElseWith(fun () -> makeUserImageUrl "wavatar" name)
let identity = Person {oauthId = Some id; email = None; name = None}
let user = {ChatUser.makeNew identity name with imageUrl = imageUrl}

Expand All @@ -305,11 +300,11 @@ let oauthCallbackHandler (provider: string) : HttpFunc -> HttpFunc =
ctx.Session.SetString("userid", userid)
return! redirectTo false "/" next ctx
| Result.Error message ->
return! text (sprintf "Register failed because of `%s`" message) next ctx
return! text(sprintf "Register failed because of `%s`" message) next ctx
else
return! text "OAuth authentication failed" next ctx
with
| ex -> return! text (sprintf "OAuth error: %s" ex.Message) next ctx
| ex -> return! text(sprintf "OAuth error: %s" ex.Message) next ctx
| None ->
return! text "Server not initialized" next ctx
}
Expand Down Expand Up @@ -339,6 +334,9 @@ let webApp : HttpFunc -> HttpFunc =
// ---------------------------------

let configureServices (services: IServiceCollection) =
// Create temporary logger for service configuration
let loggerFactory = LoggerFactory.Create(fun builder -> builder.AddConsole() |> ignore)
let logger = loggerFactory.CreateLogger("service-config")
// Add session support
services.AddDistributedMemoryCache() |> ignore
services.AddSession(fun options ->
Expand All @@ -365,7 +363,7 @@ let configureServices (services: IServiceCollection) =
options.CallbackPath <- "/oauth/callback/google"
) |> ignore
with
| _ -> printfn "OAuth configuration not found or invalid"
| _ -> logger.LogWarning("OAuth configuration not found or invalid")

services.AddGiraffe() |> ignore

Expand Down
11 changes: 6 additions & 5 deletions src/Server/Diag.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ module Diag

open Akka.Actor
open Akkling
open Microsoft.Extensions.Logging

open ChatUser
open ChatTypes
Expand Down Expand Up @@ -43,7 +44,7 @@ let createEchoActor (getUser: GetUser) (system: ActorSystem) (botUserId: UserId)
in
spawn system "echobot" <| props(handler)

let createDiagChannel (getUser: GetUser) (system: ActorSystem) (server: IActorRef<_>) (echoUserId, channelName, topic) =
let createDiagChannel (logger: ILogger) (getUser: GetUser) (system: ActorSystem) (server: IActorRef<_>) (echoUserId, channelName, topic) =
async {
let bot = createEchoActor getUser system echoUserId
let chanActorProps = GroupChatChannelActor.props None
Expand All @@ -54,8 +55,8 @@ let createDiagChannel (getUser: GetUser) (system: ActorSystem) (server: IActorRe
let! channel = server |> getChannel (fun chan -> chan.cid = chanId)
match channel with
| Ok chan -> chan.channelActor <! ChannelCommand (NewParticipant (echoUserId, bot))
| Error _ ->
() // FIXME log error
| Error _ ->
() // FIXME log error
| Error err ->
logger.LogError("Failed to get channel {chanId}: {error}", chanId, err)
| Error err ->
logger.LogError("Failed to create/get channel {channelName}: {error}", channelName, err)
}
Loading
Loading