Skip to content

Commit

Permalink
Merge pull request #57 from andresharpe/feature/graphql-chat-improvemets
Browse files Browse the repository at this point in the history
Refined prompts and display
  • Loading branch information
andresharpe authored Oct 14, 2024
2 parents 0a306a4 + 87cf7e8 commit 5871ccf
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 52 deletions.
55 changes: 55 additions & 0 deletions source/Cute.Lib/Extensions/JTokenExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
using System.Text;
using Newtonsoft.Json.Linq;

public static class JTokenExtensions
{
public static string ToUserFriendlyString(this JToken token, int indentLevel = 0)
{
var sb = new StringBuilder();
WriteYaml(token, sb, indentLevel);
return sb.ToString();
}

private static void WriteYaml(JToken? token, StringBuilder sb, int indentLevel)
{
if (token == null) return;
switch (token.Type)
{
case JTokenType.Object:
foreach (var property in (JObject)token)
{
AppendIndent(sb, indentLevel);
sb.Append(property.Key).Append(":");
if (property.Value is JObject || property.Value is JArray)
{
sb.AppendLine();
WriteYaml(property.Value, sb, indentLevel + 1);
}
else
{
sb.Append(" ");
WriteYaml(property.Value, sb, indentLevel);
}
}
break;

case JTokenType.Array:
foreach (var item in (JArray)token)
{
AppendIndent(sb, indentLevel);
sb.AppendLine("-");
WriteYaml(item, sb, indentLevel + 1);
}
break;

default:
sb.AppendLine(token.ToString());
break;
}
}

private static void AppendIndent(StringBuilder sb, int indentLevel)
{
sb.Append(new string(' ', indentLevel * 2));
}
}
107 changes: 64 additions & 43 deletions source/Cute/Commands/Chat/ChatCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
using Cute.Lib.AiModels;
using Cute.Lib.Contentful;
using Cute.Lib.Contentful.CommandModels.ContentGenerateCommand;
using Cute.Lib.Contentful.GraphQL;
using Cute.Lib.Enums;
using Cute.Lib.Exceptions;
using Cute.Lib.Extensions;
Expand Down Expand Up @@ -160,7 +159,11 @@ await AnsiConsole.Status()
{
_console.WriteBlankLine();
_console.WriteNormalWithHighlights(
$"Greetings, traveler of the '{defaultSpace.Name}' space! I'm Douglas, your guide to the wonders of Contentful.",
$"Greetings, traveler of the '{defaultSpace.Name}' space, wanderer of the '{defaultEnvironment.Id()}' environment!",
Globals.StyleHeading);
_console.WriteBlankLine();
_console.WriteNormalWithHighlights(
$"I'm Douglas, your guide to the wonders of Contentful.",
Globals.StyleHeading);
_console.WriteBlankLine();
_console.WriteNormalWithHighlights(
Expand Down Expand Up @@ -205,12 +208,9 @@ await AnsiConsole.Status()
_console.WriteRuler();
}

_console.WriteBlankLine();

if (!isDouglas)
{
_console.WriteNormalWithHighlights($"Press {"<Tab>"} or {"<Ctrl+Enter>"} to submit your prompt.", Globals.StyleHeading);
_console.WriteBlankLine();
}

List<ChatMessage> messages = [new SystemChatMessage(systemMessage)];
Expand Down Expand Up @@ -280,34 +280,19 @@ await AnsiConsole.Status()

if (botResponse.Answer is not null)
{
if (isDouglas)
{
_console.WriteSubHeading(botResponse.Answer);
DisplayResponseCopyLink(botResponse.Answer);
MarkdownConsole.Write(botResponse.Answer);

if (botResponse.ContentInfo is null
&& !string.IsNullOrEmpty(botResponse.ContentTypeId)
&& string.IsNullOrEmpty(botResponse.QueryOrCommand)
&& lastContentInfoPromptAdded != botResponse.ContentTypeId)
{
await BuildContentTypeGraphQLPromptInfo(botResponse);
if (botResponse.ContentInfo is not null)
{
var contentInfo = botResponse.ContentInfo.ToString();
messages.Add(new SystemChatMessage(contentInfo));
lastContentInfoPromptAdded = botResponse.ContentTypeId;
}
}
}
else
if (isDouglas
&& lastContentInfoPromptAdded != botResponse.ContentTypeId
&& !string.IsNullOrWhiteSpace(botResponse.ContentTypeId))
{
if (settings.Verbosity >= Verbosity.Diagnostic)
await BuildContentTypeGraphQLPromptInfo(botResponse);
if (botResponse.ContentInfo is not null)
{
_console.WriteDim(botResponse.Answer);
_console.WriteBlankLine();
messages.Add(new SystemChatMessage(botResponse.ContentInfo.ToString()));
}

DisplayResponseCopyLink(botResponse.Answer);
MarkdownConsole.Write(botResponse.Answer);
lastContentInfoPromptAdded = botResponse.ContentTypeId;
}
}

Expand All @@ -333,6 +318,18 @@ await AnsiConsole.Status()
{ex.InnerException?.Message}
""";

try
{
var info = JsonConvert.DeserializeObject<JObject>(ex.Message);
if (info is not null)
{
_console.WriteDim(info.ToUserFriendlyString() ?? string.Empty);
_console.WriteBlankLine();
}
}
catch
{ // ignore
}
_console.WriteAlert("The operation returned an error. Don't panic! (the details have been shared with Douglas)...");
}
continue;
Expand All @@ -341,7 +338,7 @@ await AnsiConsole.Status()
if (botResponse.Question is not null)
{
_console.WriteBlankLine();
_console.WriteSubHeading(botResponse.Question);
MarkdownConsole.Write(botResponse.Question);
}
}

Expand Down Expand Up @@ -386,6 +383,12 @@ private async Task HandleExecution(IEnumerable<Locale> locales, BotResponse botR
DisplayQueryOrCommandCopyLink(botResponse.QueryOrCommand);
_console.WriteAlertAccent(botResponse.QueryOrCommand);
_console.WriteBlankLine();

var env = (await ContentfulConnection.GetDefaultEnvironmentAsync()).Id();
var space = (await ContentfulConnection.GetDefaultSpaceAsync()).Name;
var spaceConfirmation = $"[italic {Globals.StyleDim.Foreground}]...the query will run in the '[{Globals.StyleSubHeading.Foreground}]{env}[/]' environment of space '[{Globals.StyleSubHeading.Foreground}]{space}[/]'.[/]";
AnsiConsole.MarkupLine(spaceConfirmation);

_console.WriteRuler();
_console.WriteBlankLine();

Expand Down Expand Up @@ -423,10 +426,10 @@ await AnsiConsole.Status()
.StartAsync("Executing GraphQL query...", async ctx =>
{
_console.WriteBlankLine();
var contentTypeId = GraphQLUtilities.GetContentTypeId(botResponse.QueryOrCommand);
var jsonPath = $"$.data.{contentTypeId}Collection";
var jsonPath = $"..{botResponse.ContentTypeId}Collection";
var localeCode = locales.Where(l => l.Default).First().Code;
await foreach (var result in ContentfulConnection.GraphQL.GetRawDataEnumerable(botResponse.QueryOrCommand, localeCode, preview: true))
await foreach (var result in ContentfulConnection.GraphQL.GetRawDataEnumerable(
botResponse.QueryOrCommand, localeCode, preview: true))
{
var node = result.SelectToken(jsonPath);
if (node is null) continue;
Expand Down Expand Up @@ -491,11 +494,11 @@ The user can type "bye" or "exit" to end the conversation at any time.

Today is {{DateTime.UtcNow:R}}.

Structure every response as a JSON document with keys "answer", "question", "queryOrCommand", "type".
"answer" is a JSON string and contains your best answer. Keep them punchy.
"question" is a JSON string and contains your next question for the user to help them reach their goal.
"queryOrCommand" is a JSON string and contains the accurate CLI command or GraphQl query that will achieve the goal.
"contentTypeId" is a JSON string and contains the root content type the user is interested in. Populate this as early as possible.
Structure every response as a JSON document with keys "answer", "question", "queryOrCommand", "type", "contentTypeId".
"answer" is a Markdown containting your best answer. Keep them punchy.
"question" contains your next question for the user to help them reach their goal in Markdown.
"queryOrCommand" contains the accurate CLI command or GraphQl query that will achieve the goal.
"contentTypeId" is a string and contains the root content type the user is interested in. Populate this as early as possible.
"type" contains "GraphQL" or "CLI" to execute commands depending on what is in "queryOrCommand".
"type" contains "Exit" if the user wants to leave the conversation or quit the app.
"queryOrCommand" and "type" MUST only supplied when you ask "Shall we give it a shot?" when the goal is clear to you.
Expand All @@ -519,13 +522,33 @@ Field names ending with "Entries" are links to a multiple entry in a content typ
All content type and field names MUST match the schema. Correct user input and spelling automatically.

Make sure GraphQl queries are correct, fields are cased exactly right and query is prettified on multiple lines.
For types that contain a "key" and/or "title" field, you MUST ALWAYS include these fields in your final query.
For types that contain a "key" and/or "title" field, these are the default fields in your query if the user doesn't suggest any fields.
Only use valid GraphQL that conforms to the Contentful GraphQL API spec.
Always include "Lat" and "Lon" subfields for "Location" type fields.
Here is an **example** of a well formed Contentful GraphQL query for a content type named "dataCountry":
"""
query GetContent($preview: Boolean, $skip: Int, $limit: Int) {
dataCountryCollection(preview: $peview, skip: $skip, limit: $limit) {
query ($preview: Boolean, $skip: Int, $limit: Int) {
dataCountryCollection(preview: $preview, skip: $skip, limit: $limit) {
items {
key
title
iso2Code
phoneCode
population
flag {
url
}
}
}
}
"""
Add a $preview, $limit and $skip parameter for the outer query and reference them in the inner query.

Only if the user implies or asks for a limited number of entries, remove the outer $limit and specify the limit,
For example, of the user wants 10 entries then the corect query will be:
"""
query ($preview: Boolean, $skip: Int) {
dataCountryCollection(preview: $preview, skip: $skip, limit: 10) {
items {
key
title
Expand All @@ -539,8 +562,6 @@ query GetContent($preview: Boolean, $skip: Int, $limit: Int) {
}
}
"""
Always accept and pass on $preview, $limit and $skip parameters.
If you are asked for a specific number of entries, you can override their values in the Collection with scalar values.

The valid content types and fields that are available in this Contentful space are contained in the following quoted text:

Expand Down
30 changes: 21 additions & 9 deletions source/Cute/Services/ConsoleWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,8 @@ public void WriteTable(JArray jArray)
CollectColumns(obj, string.Empty, columns);
}

table.AddColumn(new TableColumn("#").RightAligned());

foreach (var column in columns)
{
var firstValue = GetNestedValue((JObject)jArray.First(), column);
Expand All @@ -223,10 +225,13 @@ public void WriteTable(JArray jArray)
foreach (JObject obj in jArray.Cast<JObject>())
{
var row = columns
.Select((column, colNumber) =>
.Select(column =>
FormatCell(
GetNestedValue(obj, column),
rowNumber, colNumber, colNumber == columns.Count - 1));
GetNestedValue(obj, column), rowNumber)
)
.ToList();

row.Insert(0, new Markup(rowNumber.ToString(), Globals.StyleDim).RightJustified());

table.AddRow(row);

Expand Down Expand Up @@ -284,25 +289,32 @@ private static void CollectColumns(JObject obj, string prefix, HashSet<string> c
return current;
}

private static Markup FormatCell(JToken? token, int rowNumber, int colNumber, bool isLastColumn)
private static Markup FormatCell(JToken? token, int rowNumber)
{
var isEvenRow = rowNumber % 2 == 0;
var style = isEvenRow ? Globals.StyleNormal : Globals.StyleSubHeading;
var isOddRow = rowNumber % 2 == 1;
var style = isOddRow ? Globals.StyleSubHeading : Globals.StyleNormal;

if (token == null)
return new Markup(string.Empty);

if (token.Type == JTokenType.Integer)
{
// Format integers without any decimal places
return new Markup(Convert.ToInt32(token).ToString("N0", CultureInfo.InvariantCulture), style).RightJustified();
}
else if (token.Type == JTokenType.Float)
{
// Format floats with up to six decimal places
return new Markup(Convert.ToDecimal(token).ToString("N6", CultureInfo.InvariantCulture), style).RightJustified();
}
// Return other values as is
else if (token.Type == JTokenType.Date)
{
var date = token.ToObject<DateTime>();

var formattedDate = date.Hour == 0 && date.Minute == 0 && date.Second == 0
? date.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture)
: date.ToString("yyyy-MM-dd HH:mm:ss", CultureInfo.InvariantCulture);

return new Markup(formattedDate, style);
}
return new Markup(token.ToString().EscapeMarkup(), style);
}

Expand Down

0 comments on commit 5871ccf

Please sign in to comment.