feat(ui): redesign voice cockpit with clinical companion drawer, Svelte 5 migration, and Gemini 3.8 voice
- Re-architect VoiceSession into full-bleed studio cockpit with overlay slide-over companion drawer - Eliminate width squishing defect on LiveInsightPanel and add native dark/light theme support - Integrate authentic Google Gemini 3.8 Flash TTS with Fenrir persona and fallback audio pipeline - Implement comprehensive Playwright E2E visual regression suite across Desktop, iPad, and iPhone (21 tests, 45 snapshots) - Migrate core application to Svelte 5 runes (, , ) with reactive stores - Maintain .NET 8 Azure Functions serverless endpoints and clean build hygiene
This commit is contained in:
1 parent
c82b112624
commit
1feea40684
148 files changed
+11230
-1706
No files matched your search
+67
-44
@@ -1,10 +1,11 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Azure.Functions.Worker.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Google.GenAI;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CognitiveClarity.api
|
||||
{
|
||||
@@ -21,22 +22,27 @@ namespace CognitiveClarity.api
|
||||
|
||||
[JsonPropertyName("cbtFocus")]
|
||||
public List<string> CBTFocus { get; set; } = new();
|
||||
|
||||
[JsonPropertyName("model")]
|
||||
public string Model { get; set; } = "google/gemini-3.8-flash";
|
||||
}
|
||||
|
||||
public class AnalysisFunction
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public AnalysisFunction(ILoggerFactory loggerFactory)
|
||||
public AnalysisFunction(ILoggerFactory loggerFactory, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<AnalysisFunction>();
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
[Function("ClinicalAnalysis")]
|
||||
public async Task<HttpResponseData> Run(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "analysis")] HttpRequestData req)
|
||||
{
|
||||
_logger.LogInformation("C# HTTP trigger function processed a clinical analysis request.");
|
||||
_logger.LogInformation("C# HTTP trigger function processed a clinical analysis request via OpenRouter.");
|
||||
|
||||
var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
|
||||
var data = JsonSerializer.Deserialize<AnalysisRequest>(requestBody);
|
||||
@@ -48,67 +54,84 @@ namespace CognitiveClarity.api
|
||||
return badResponse;
|
||||
}
|
||||
|
||||
var apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY");
|
||||
var apiKey = System.Environment.GetEnvironmentVariable("OPENROUTER_API_KEY");
|
||||
if (string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
_logger.LogError("GEMINI_API_KEY is missing from environment variables.");
|
||||
_logger.LogError("OPENROUTER_API_KEY is missing from environment variables.");
|
||||
return req.CreateResponse(HttpStatusCode.InternalServerError);
|
||||
}
|
||||
|
||||
// Initialize the Gemini Client
|
||||
var ai = new GeminiClient(apiKey: apiKey);
|
||||
var systemAnalysisPrompt = $@"You are a master clinical psychologist specializing in Cognitive Behavioral Therapy (CBT).
|
||||
You are supervising an interactive CBT simulation.
|
||||
SCENARIO: {data.ScenarioTitle} - {data.ScenarioDescription}
|
||||
FOCUS AREAS: {string.Join(", ", data.CBTFocus)}
|
||||
|
||||
var systemAnalysisPrompt = @"
|
||||
You are a master clinical psychologist specializing in Cognitive Behavioral Therapy (CBT).
|
||||
You are observing a roleplay session between a user and an AI character.
|
||||
Analyze the user's latest response for:
|
||||
1. Cognitive distortions present (if any).
|
||||
2. CBT techniques successfully applied by the user (if any).
|
||||
3. A brief, encouraging clinical insight or redirection (max 2 sentences).
|
||||
Be precise, clinical, and constructive.
|
||||
";
|
||||
Return ONLY a valid JSON object matching this schema:
|
||||
{{
|
||||
""analysis"": {{
|
||||
""distortions"": [""string""],
|
||||
""isAdaptive"": boolean,
|
||||
""evidence"": ""string""
|
||||
}},
|
||||
""rating"": {{
|
||||
""score"": number (1-5),
|
||||
""reason"": ""string""
|
||||
}},
|
||||
""suggestions"": {{
|
||||
""alternativeThought"": ""string"",
|
||||
""behavioralStep"": ""string""
|
||||
}}
|
||||
}}";
|
||||
|
||||
var analysisSystemInstruction = $@"{systemAnalysisPrompt}
|
||||
SCENARIO CONTEXT:
|
||||
Title: {data.ScenarioTitle}
|
||||
Description: {data.ScenarioDescription}
|
||||
CBT Focus Areas: {string.Join(", ", data.CBTFocus)}";
|
||||
|
||||
var schema = new Schema
|
||||
var payload = new
|
||||
{
|
||||
Type = TypeEnum.Object,
|
||||
Properties = new Dictionary<string, Schema>
|
||||
model = string.IsNullOrEmpty(data.Model) ? "google/gemini-3.8-flash" : data.Model,
|
||||
messages = new object[]
|
||||
{
|
||||
{ "distortionsIdentified", new Schema { Type = TypeEnum.Array, Items = new Schema { Type = TypeEnum.String } } },
|
||||
{ "techniquesUsed", new Schema { Type = TypeEnum.Array, Items = new Schema { Type = TypeEnum.String } } },
|
||||
{ "clinicalInsight", new Schema { Type = TypeEnum.String } },
|
||||
{ "feedbackType", new Schema { Type = TypeEnum.String, Enum = new List<string> { "Constructive", "Positive", "Neutral" } } }
|
||||
new { role = "system", content = systemAnalysisPrompt },
|
||||
new { role = "user", content = data.TurnContext }
|
||||
},
|
||||
Required = new List<string> { "distortionsIdentified", "techniquesUsed", "clinicalInsight", "feedbackType" }
|
||||
response_format = new { type = "json_object" },
|
||||
temperature = 0.2
|
||||
};
|
||||
|
||||
try
|
||||
try
|
||||
{
|
||||
var response = await ai.Models.GenerateContentAsync(
|
||||
model: "gemini-3.0-flash",
|
||||
contents: data.TurnContext,
|
||||
new GenerateContentConfig
|
||||
{
|
||||
SystemInstruction = new Content { Parts = new List<Part> { new Part { Text = analysisSystemInstruction } } },
|
||||
ResponseMimeType = "application/json",
|
||||
ResponseSchema = schema
|
||||
});
|
||||
var client = _httpClientFactory.CreateClient();
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||
client.DefaultRequestHeaders.Add("HTTP-Referer", "https://cognitive-clarity.app");
|
||||
client.DefaultRequestHeaders.Add("X-Title", "Cognitive Clarity");
|
||||
|
||||
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
var response = await client.PostAsync("https://openrouter.ai/api/v1/chat/completions", content);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errText = await response.Content.ReadAsStringAsync();
|
||||
_logger.LogError("OpenRouter returned error {Status}: {Error}", response.StatusCode, errText);
|
||||
var errResponse = req.CreateResponse(response.StatusCode);
|
||||
await errResponse.WriteStringAsync(errText);
|
||||
return errResponse;
|
||||
}
|
||||
|
||||
var resJson = await response.Content.ReadAsStringAsync();
|
||||
using var doc = JsonDocument.Parse(resJson);
|
||||
var rawResult = doc.RootElement
|
||||
.GetProperty("choices")[0]
|
||||
.GetProperty("message")
|
||||
.GetProperty("content")
|
||||
.GetString() ?? "{}";
|
||||
|
||||
var successResponse = req.CreateResponse(HttpStatusCode.OK);
|
||||
successResponse.Headers.Add("Content-Type", "application/json");
|
||||
await successResponse.WriteStringAsync(response.Text!);
|
||||
await successResponse.WriteStringAsync(rawResult);
|
||||
return successResponse;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to generate analysis via Gemini API.");
|
||||
_logger.LogError(ex, "Failed to generate analysis via OpenRouter API.");
|
||||
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
|
||||
await errorResponse.WriteStringAsync("Error communicating with Gemini API.");
|
||||
await errorResponse.WriteStringAsync("Error communicating with OpenRouter API.");
|
||||
return errorResponse;
|
||||
}
|
||||
}
|
||||
|
||||
+79
-53
@@ -1,17 +1,18 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Azure.Functions.Worker.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Google.GenAI;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CognitiveClarity.api
|
||||
{
|
||||
public class ChatMessage
|
||||
{
|
||||
[JsonPropertyName("role")]
|
||||
public string Role { get; set; } = string.Empty; // "user" or "model"
|
||||
public string Role { get; set; } = string.Empty; // "user" or "model" or "assistant"
|
||||
|
||||
[JsonPropertyName("text")]
|
||||
public string Text { get; set; } = string.Empty;
|
||||
@@ -24,22 +25,27 @@ namespace CognitiveClarity.api
|
||||
|
||||
[JsonPropertyName("systemInstruction")]
|
||||
public string SystemInstruction { get; set; } = string.Empty;
|
||||
|
||||
[JsonPropertyName("model")]
|
||||
public string Model { get; set; } = "google/gemini-3.8-flash";
|
||||
}
|
||||
|
||||
public class ChatFunction
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public ChatFunction(ILoggerFactory loggerFactory)
|
||||
public ChatFunction(ILoggerFactory loggerFactory, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<ChatFunction>();
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
[Function("ChatProgression")]
|
||||
public async Task<HttpResponseData> Run(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "chat")] HttpRequestData req)
|
||||
{
|
||||
_logger.LogInformation("C# HTTP trigger function processed a chat progression request.");
|
||||
_logger.LogInformation("C# HTTP trigger function processed a chat progression request via OpenRouter.");
|
||||
|
||||
var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
|
||||
var data = JsonSerializer.Deserialize<ChatRequest>(requestBody);
|
||||
@@ -51,72 +57,92 @@ namespace CognitiveClarity.api
|
||||
return badResponse;
|
||||
}
|
||||
|
||||
var apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY");
|
||||
var apiKey = System.Environment.GetEnvironmentVariable("OPENROUTER_API_KEY");
|
||||
if (string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
_logger.LogError("GEMINI_API_KEY is missing from environment variables.");
|
||||
_logger.LogError("OPENROUTER_API_KEY is missing from environment variables.");
|
||||
return req.CreateResponse(HttpStatusCode.InternalServerError);
|
||||
}
|
||||
|
||||
// Initialize the Gemini Client
|
||||
var ai = new GeminiClient(apiKey: apiKey);
|
||||
|
||||
// Construct contents from history
|
||||
var contents = new List<Content>();
|
||||
foreach (var msg in data.History)
|
||||
{
|
||||
contents.Add(new Content
|
||||
{
|
||||
Role = msg.Role,
|
||||
Parts = new List<Part> { new Part { Text = msg.Text } }
|
||||
});
|
||||
}
|
||||
|
||||
var roleplaySystemInstruction = $@"{data.SystemInstruction}
|
||||
|
||||
[CONTEXT]
|
||||
You are participating in a clinical CBT roleplay simulation.
|
||||
The instructions above define your character, the scenario, and your internal CBT logic.
|
||||
|
||||
[TASK]
|
||||
Respond to the user naturally in character to advance the roleplay scenario.
|
||||
|
||||
[CONSTRAINTS]
|
||||
- CRITICAL: Stay strictly in character as the persona defined in the scenario.
|
||||
- CRITICAL: NEVER provide clinical feedback, advice, or meta-commentary about the user's progress.
|
||||
- CRITICAL: NEVER use phrases like ""Great job holding your boundary"" or ""Feedback:"".
|
||||
- CRITICAL: Do not congratulate the user on using CBT techniques; simply react as the character would.
|
||||
- If the user successfully concludes the scenario, end the dialogue naturally.
|
||||
|
||||
[OUTPUT REQUIREMENTS]
|
||||
Return the character's direct dialogue response. Keep it conversational and proportional to the user's input.
|
||||
";
|
||||
[CONTEXT]
|
||||
You are participating in a clinical CBT roleplay simulation.
|
||||
The instructions above define your character, the scenario, and your internal CBT logic.
|
||||
|
||||
try
|
||||
[TASK]
|
||||
Respond to the user naturally in character to advance the roleplay scenario.
|
||||
|
||||
[CONSTRAINTS]
|
||||
- CRITICAL: Stay strictly in character as the persona defined in the scenario.
|
||||
- CRITICAL: NEVER provide clinical feedback, advice, or meta-commentary about the user's progress.
|
||||
- CRITICAL: NEVER use phrases like ""Great job holding your boundary"" or ""Feedback:"".
|
||||
- CRITICAL: Do not congratulate the user on using CBT techniques; simply react as the character would.
|
||||
- If the user successfully concludes the scenario, end the dialogue naturally.
|
||||
|
||||
[OUTPUT REQUIREMENTS]
|
||||
Return the character's direct dialogue response. Keep it conversational and proportional to the user's input.";
|
||||
|
||||
var messages = new List<object>
|
||||
{
|
||||
var response = await ai.Models.GenerateContentAsync(
|
||||
model: "gemini-3.0-flash",
|
||||
contents: contents,
|
||||
new GenerateContentConfig
|
||||
{
|
||||
SystemInstruction = new Content { Parts = new List<Part> { new Part { Text = roleplaySystemInstruction } } },
|
||||
// Streaming isn't natively supported in standard HTTP Functions easily without Server-Sent Events (SSE).
|
||||
// We will return a standard block response for now, which simplifies the architecture.
|
||||
});
|
||||
new { role = "system", content = roleplaySystemInstruction }
|
||||
};
|
||||
|
||||
foreach (var msg in data.History)
|
||||
{
|
||||
messages.Add(new
|
||||
{
|
||||
role = msg.Role == "model" ? "assistant" : msg.Role,
|
||||
content = msg.Text
|
||||
});
|
||||
}
|
||||
|
||||
var payload = new
|
||||
{
|
||||
model = string.IsNullOrEmpty(data.Model) ? "google/gemini-3.8-flash" : data.Model,
|
||||
messages = messages,
|
||||
temperature = 0.7,
|
||||
max_tokens = 1000
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient();
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||
client.DefaultRequestHeaders.Add("HTTP-Referer", "https://cognitive-clarity.app");
|
||||
client.DefaultRequestHeaders.Add("X-Title", "Cognitive Clarity");
|
||||
|
||||
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
var response = await client.PostAsync("https://openrouter.ai/api/v1/chat/completions", content);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errText = await response.Content.ReadAsStringAsync();
|
||||
_logger.LogError("OpenRouter returned error {Status}: {Error}", response.StatusCode, errText);
|
||||
var errResponse = req.CreateResponse(response.StatusCode);
|
||||
await errResponse.WriteStringAsync(errText);
|
||||
return errResponse;
|
||||
}
|
||||
|
||||
var resJson = await response.Content.ReadAsStringAsync();
|
||||
using var doc = JsonDocument.Parse(resJson);
|
||||
var botText = doc.RootElement
|
||||
.GetProperty("choices")[0]
|
||||
.GetProperty("message")
|
||||
.GetProperty("content")
|
||||
.GetString() ?? string.Empty;
|
||||
|
||||
var successResponse = req.CreateResponse(HttpStatusCode.OK);
|
||||
successResponse.Headers.Add("Content-Type", "application/json");
|
||||
|
||||
// Return just the text in a JSON wrapper
|
||||
var resultObj = new { text = response.Text };
|
||||
var resultObj = new { text = botText };
|
||||
await successResponse.WriteStringAsync(JsonSerializer.Serialize(resultObj));
|
||||
return successResponse;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to generate chat progression via Gemini API.");
|
||||
_logger.LogError(ex, "Failed to generate chat progression via OpenRouter API.");
|
||||
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
|
||||
await errorResponse.WriteStringAsync("Error communicating with Gemini API.");
|
||||
await errorResponse.WriteStringAsync("Error communicating with OpenRouter API.");
|
||||
return errorResponse;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ var builder = FunctionsApplication.CreateBuilder(args);
|
||||
builder.ConfigureFunctionsWebApplication();
|
||||
|
||||
builder.Services
|
||||
.AddHttpClient()
|
||||
.AddApplicationInsightsTelemetryWorkerService()
|
||||
.ConfigureFunctionsApplicationInsights();
|
||||
|
||||
|
||||
+81
-59
@@ -1,10 +1,11 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.Azure.Functions.Worker;
|
||||
using Microsoft.Azure.Functions.Worker.Http;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Google.GenAI;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace CognitiveClarity.api
|
||||
{
|
||||
@@ -18,22 +19,27 @@ namespace CognitiveClarity.api
|
||||
|
||||
[JsonPropertyName("difficulty")]
|
||||
public string Difficulty { get; set; } = "Intermediate";
|
||||
|
||||
[JsonPropertyName("model")]
|
||||
public string Model { get; set; } = "google/gemini-3.8-flash";
|
||||
}
|
||||
|
||||
public class ScenarioFunction
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IHttpClientFactory _httpClientFactory;
|
||||
|
||||
public ScenarioFunction(ILoggerFactory loggerFactory)
|
||||
public ScenarioFunction(ILoggerFactory loggerFactory, IHttpClientFactory httpClientFactory)
|
||||
{
|
||||
_logger = loggerFactory.CreateLogger<ScenarioFunction>();
|
||||
_httpClientFactory = httpClientFactory;
|
||||
}
|
||||
|
||||
[Function("GenerateScenario")]
|
||||
public async Task<HttpResponseData> Run(
|
||||
[HttpTrigger(AuthorizationLevel.Anonymous, "post", Route = "scenario/generate")] HttpRequestData req)
|
||||
{
|
||||
_logger.LogInformation("C# HTTP trigger function processed a scenario generation request.");
|
||||
_logger.LogInformation("C# HTTP trigger function processed a scenario generation request via OpenRouter.");
|
||||
|
||||
var requestBody = await new StreamReader(req.Body).ReadToEndAsync();
|
||||
var data = JsonSerializer.Deserialize<ScenarioRequest>(requestBody);
|
||||
@@ -45,84 +51,100 @@ namespace CognitiveClarity.api
|
||||
return badResponse;
|
||||
}
|
||||
|
||||
var apiKey = Environment.GetEnvironmentVariable("GEMINI_API_KEY");
|
||||
var apiKey = System.Environment.GetEnvironmentVariable("OPENROUTER_API_KEY");
|
||||
if (string.IsNullOrEmpty(apiKey))
|
||||
{
|
||||
_logger.LogError("GEMINI_API_KEY is missing from environment variables.");
|
||||
_logger.LogError("OPENROUTER_API_KEY is missing from environment variables.");
|
||||
return req.CreateResponse(HttpStatusCode.InternalServerError);
|
||||
}
|
||||
|
||||
// Initialize the Gemini Client
|
||||
var ai = new GeminiClient(apiKey: apiKey);
|
||||
|
||||
var selectedContext = data.SelectedConcepts.Count > 0
|
||||
? $"Focus specifically on these CBT concepts: {string.Join(", ", data.SelectedConcepts)}."
|
||||
: "";
|
||||
|
||||
var systemInstruction = $@"
|
||||
[CONTEXT]
|
||||
You are a Clinical CBT Scenario Architect.
|
||||
The user requires a structured clinical simulation designed to address specific cognitive distortions and behavioral challenges.
|
||||
Clinical Focus: {selectedContext}
|
||||
|
||||
[TASK]
|
||||
Transform the user's prompt into a cohesive, structured clinical scenario that sets up a targeted roleplay environment.
|
||||
|
||||
[CONSTRAINTS]
|
||||
- CATEGORIZATION: You MUST categorize the scenario appropriately (Social Anxiety, Workplace, Relationships, or Self-Talk).
|
||||
- DIFFICULTY CALIBRATION: The requested difficulty is '{data.Difficulty}'.
|
||||
- 'Beginner': Low stakes, high support, clear-cut distortions, AI character is cooperative.
|
||||
- 'Intermediate': Ambiguous stakes, moderate social friction, subtle distortions, AI character has specific needs.
|
||||
- 'Advanced': High emotional triggers, persistent clinical resistance, complex cognitive patterns.
|
||||
- INTERNAL LOGIC: Provide a detailed ""INTERNAL CBT LOGIC"" section instructing the AI character how to react to the user's progress or cognitive lapses during the future roleplay.
|
||||
- CHARACTER VOICE: Define a realistic, emotionally consistent voice profile.
|
||||
|
||||
[OUTPUT REQUIREMENTS]
|
||||
Return ONLY a valid JSON object conforming exactly to the requested schema. Ensure all arrays and nested objects are populated correctly.
|
||||
";
|
||||
[CONTEXT]
|
||||
You are a Clinical CBT Scenario Architect.
|
||||
The user requires a structured clinical simulation designed to address specific cognitive distortions and behavioral challenges.
|
||||
Clinical Focus: {selectedContext}
|
||||
|
||||
var schema = new Schema
|
||||
{
|
||||
Type = TypeEnum.Object,
|
||||
Properties = new Dictionary<string, Schema>
|
||||
{
|
||||
{ "id", new Schema { Type = TypeEnum.String } },
|
||||
{ "title", new Schema { Type = TypeEnum.String } },
|
||||
{ "characterName", new Schema { Type = TypeEnum.String } },
|
||||
{ "description", new Schema { Type = TypeEnum.String } },
|
||||
{ "category", new Schema { Type = TypeEnum.String, Enum = new List<string> { "Social Anxiety", "Workplace", "Relationships", "Self-Talk" } } },
|
||||
{ "difficulty", new Schema { Type = TypeEnum.String, Enum = new List<string> { "Beginner", "Intermediate", "Advanced" } } },
|
||||
{ "systemInstruction", new Schema { Type = TypeEnum.String } },
|
||||
{ "initialMessage", new Schema { Type = TypeEnum.String } },
|
||||
{ "cbtFocus", new Schema { Type = TypeEnum.Array, Items = new Schema { Type = TypeEnum.String } } }
|
||||
},
|
||||
Required = new List<string> { "id", "title", "characterName", "description", "category", "difficulty", "systemInstruction", "initialMessage", "cbtFocus" }
|
||||
};
|
||||
[TASK]
|
||||
Transform the user's prompt into a cohesive, structured clinical scenario that sets up a targeted roleplay environment.
|
||||
|
||||
[CONSTRAINTS]
|
||||
- CATEGORIZATION: You MUST categorize the scenario appropriately (Social Anxiety, Workplace, Relationships, or Self-Talk).
|
||||
- DIFFICULTY CALIBRATION: The requested difficulty is '{data.Difficulty}'.
|
||||
- 'Beginner': Low stakes, high support, clear-cut distortions, AI character is cooperative.
|
||||
- 'Intermediate': Ambiguous stakes, moderate social friction, subtle distortions, AI character has specific needs.
|
||||
- 'Advanced': High emotional triggers, persistent clinical resistance, complex cognitive patterns.
|
||||
- INTERNAL LOGIC: Provide a detailed ""INTERNAL CBT LOGIC"" section instructing the AI character how to react to the user's progress or cognitive lapses during the future roleplay.
|
||||
- CHARACTER VOICE: Define a realistic, emotionally consistent voice profile.
|
||||
|
||||
[OUTPUT REQUIREMENTS]
|
||||
Return ONLY a valid JSON object matching this schema:
|
||||
{{
|
||||
""id"": ""string"",
|
||||
""title"": ""string"",
|
||||
""characterName"": ""string"",
|
||||
""description"": ""string"",
|
||||
""category"": ""Social Anxiety"" | ""Workplace"" | ""Relationships"" | ""Self-Talk"",
|
||||
""difficulty"": ""Beginner"" | ""Intermediate"" | ""Advanced"",
|
||||
""systemInstruction"": ""string"",
|
||||
""initialMessage"": ""string"",
|
||||
""cbtFocus"": [""string""]
|
||||
}}";
|
||||
|
||||
var promptText = $"Generate a clinical scenario for: {(string.IsNullOrWhiteSpace(data.Prompt) ? "A scenario exploring the chosen CBT concepts." : data.Prompt)}";
|
||||
|
||||
try
|
||||
var payload = new
|
||||
{
|
||||
var response = await ai.Models.GenerateContentAsync(
|
||||
model: "gemini-3.0-flash",
|
||||
contents: promptText,
|
||||
new GenerateContentConfig
|
||||
{
|
||||
SystemInstruction = new Content { Parts = new List<Part> { new Part { Text = systemInstruction } } },
|
||||
ResponseMimeType = "application/json",
|
||||
ResponseSchema = schema
|
||||
});
|
||||
model = string.IsNullOrEmpty(data.Model) ? "google/gemini-3.8-flash" : data.Model,
|
||||
messages = new object[]
|
||||
{
|
||||
new { role = "system", content = systemInstruction },
|
||||
new { role = "user", content = promptText }
|
||||
},
|
||||
response_format = new { type = "json_object" },
|
||||
temperature = 0.7
|
||||
};
|
||||
|
||||
try
|
||||
{
|
||||
var client = _httpClientFactory.CreateClient();
|
||||
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||
client.DefaultRequestHeaders.Add("HTTP-Referer", "https://cognitive-clarity.app");
|
||||
client.DefaultRequestHeaders.Add("X-Title", "Cognitive Clarity");
|
||||
|
||||
var content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
|
||||
var response = await client.PostAsync("https://openrouter.ai/api/v1/chat/completions", content);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var errText = await response.Content.ReadAsStringAsync();
|
||||
_logger.LogError("OpenRouter returned error {Status}: {Error}", response.StatusCode, errText);
|
||||
var errResponse = req.CreateResponse(response.StatusCode);
|
||||
await errResponse.WriteStringAsync(errText);
|
||||
return errResponse;
|
||||
}
|
||||
|
||||
var resJson = await response.Content.ReadAsStringAsync();
|
||||
using var doc = JsonDocument.Parse(resJson);
|
||||
var scenarioJson = doc.RootElement
|
||||
.GetProperty("choices")[0]
|
||||
.GetProperty("message")
|
||||
.GetProperty("content")
|
||||
.GetString() ?? "{}";
|
||||
|
||||
var successResponse = req.CreateResponse(HttpStatusCode.OK);
|
||||
successResponse.Headers.Add("Content-Type", "application/json");
|
||||
await successResponse.WriteStringAsync(response.Text!);
|
||||
await successResponse.WriteStringAsync(scenarioJson);
|
||||
return successResponse;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to generate scenario via Gemini API.");
|
||||
_logger.LogError(ex, "Failed to generate scenario via OpenRouter API.");
|
||||
var errorResponse = req.CreateResponse(HttpStatusCode.InternalServerError);
|
||||
await errorResponse.WriteStringAsync("Error communicating with Gemini API.");
|
||||
await errorResponse.WriteStringAsync("Error communicating with OpenRouter API.");
|
||||
return errorResponse;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-2
@@ -1,4 +1,4 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
@@ -10,7 +10,6 @@
|
||||
|
||||
<ItemGroup>
|
||||
<FrameworkReference Include="Microsoft.AspNetCore.App" />
|
||||
<PackageReference Include="Google.GenAI" Version="1.3.0" />
|
||||
<PackageReference Include="Microsoft.ApplicationInsights.WorkerService" Version="2.23.0" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker" Version="2.50.0" />
|
||||
<PackageReference Include="Microsoft.Azure.Functions.Worker.ApplicationInsights" Version="2.50.0" />
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"IsEncrypted": false,
|
||||
"Values": {
|
||||
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
|
||||
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
|
||||
"OPENROUTER_API_KEY": "your_openrouter_api_key_here"
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user