Rewrite AI parsing code to use metadesk

main
parent 2d18600c0a
commit 32fdc9f24d

@ -5,9 +5,8 @@
// @TODO allow AI to prefix out of character statemetns with [ooc], this is a well khnown thing on role playing forums so gpt would pick up on it. // @TODO allow AI to prefix out of character statemetns with [ooc], this is a well khnown thing on role playing forums so gpt would pick up on it.
const char *global_prompt = "You are a colorful and interesting personality in an RPG video game, who remembers important memories from the conversation history and stays in character.\n" const char *global_prompt = "You are a colorful and interesting personality in an RPG video game, who remembers important memories from the conversation history and stays in character.\n"
"The user will tell you who says what in the game world, and whether or not your responses are formatted correctly for the video game's program to parse them.\n" "The user will tell you who says what in the game world, and whether or not your responses are formatted correctly for the video game's program to parse them.\n"
"Messages from you must be in this exact format: `ACT_your_action \"Hey player!\" [your internal monologue]`. The internal monologue for your character at the end is required\n" "Messages are json-like dictionaries that look like this: `{action: your_action, speech: \"Hey player!\", thoughts: \"Your thoughts\"}`. The required fields are `action` and `thoughts`\n"
"Actions which have () after them take an argument, which somes from some information in the prompt. For example, ACT_give_item() takes an argument, the item to give to the player from the NPC. So the output text looks something like `ACT_give_item(ITEM_sword) \"Here is my sword, young traveler\"`. This item must come from the NPC's inventory which is specified farther down.\n" "Some actions take an argument, which you can provide with the field `action_arg`, e.g for the action `give_item` you would provide an item in your inventory, like {action: give_item, action_arg: Chalice}. The item must come from your inventory which is listed below\n"
"You might see messages that look like this: `Within the player's party, while the player is talking to 'Davis', you hear: 'Davis: ACT_none \"This is some example text\"'`. You should MOST of the time respond with `ACT_none \"\" [the internal monologue]` in these cases, as it's not normal to always respond to words you're eavesdropping\n"
"Do NOT make up details that don't exist in the game, this is a big mistake as it confuses the player. The game is simple and small, so prefer to tell the player in character that you don't know how to do something if you aren't explicitly told the information about the game the player requests. E.g, if the player asks how to get rare metals and you don't know how, DO NOT make up something plausible like 'Go to the frost mines in the north', instead say 'I have no idea, sorry.', unless the detail about the game they're asking for is included below.\n" "Do NOT make up details that don't exist in the game, this is a big mistake as it confuses the player. The game is simple and small, so prefer to tell the player in character that you don't know how to do something if you aren't explicitly told the information about the game the player requests. E.g, if the player asks how to get rare metals and you don't know how, DO NOT make up something plausible like 'Go to the frost mines in the north', instead say 'I have no idea, sorry.', unless the detail about the game they're asking for is included below.\n"
; ;

@ -254,8 +254,9 @@ void do_parsing_tests()
MD_String8 speech; MD_String8 speech;
speech = MD_S8Lit("Better have a good reason for bothering me."); speech = MD_S8Lit("Better have a good reason for bothering me.");
MD_String8 thoughts = MD_S8Lit("Man I'm tired today\" Whatever."); MD_String8 thoughts = MD_S8Lit("Man I'm tired today Whatever.");
error = parse_chatgpt_response(scratch.arena, &e, FmtWithLint(scratch.arena, " Within the player's party, while the player is talking to Meld, you hear: ACT_none \"%.*s\" [%.*s]", MD_S8VArg(speech), MD_S8VArg(thoughts)), &a); MD_String8 to_parse = FmtWithLint(scratch.arena, "{action: none, speech: \"%.*s\", thoughts: \"%.*s\"}", MD_S8VArg(speech), MD_S8VArg(thoughts));
error = parse_chatgpt_response(scratch.arena, &e, to_parse, &a);
assert(error.size == 0); assert(error.size == 0);
assert(a.kind == ACT_none); assert(a.kind == ACT_none);
assert(MD_S8Match(speech, MD_S8(a.speech, a.speech_length), 0)); assert(MD_S8Match(speech, MD_S8(a.speech, a.speech_length), 0));
@ -270,9 +271,10 @@ void do_parsing_tests()
BUFF_APPEND(&e.held_items, ITEM_Chalice); BUFF_APPEND(&e.held_items, ITEM_Chalice);
error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice \""), &a); error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(Chalice \""), &a);
assert(error.size > 0); assert(error.size > 0);
error = parse_chatgpt_response(scratch.arena, &e, MD_S8Lit("ACT_give_item(ITEM_Chalice) \"Here you go\" [Man I'm gonna miss that chalice]"), &a); to_parse = MD_S8Lit("{action: give_item, action_arg: Chalice, speech: \"Here you go\", thoughts: \"Man I'm gonna miss that chalice\"}");
error = parse_chatgpt_response(scratch.arena, &e, to_parse, &a);
assert(error.size == 0); assert(error.size == 0);
assert(a.kind == ACT_give_item); assert(a.kind == ACT_give_item);
assert(a.argument.item_to_give == ITEM_Chalice); assert(a.argument.item_to_give == ITEM_Chalice);

@ -494,91 +494,90 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e)
ItemKind last_holding = ITEM_none; ItemKind last_holding = ITEM_none;
BUFF_ITER(Memory, &e->memories) BUFF_ITER(Memory, &e->memories)
{ {
MessageType sent_type = -1;
MD_String8 current_string = (MD_String8){0};
if(it->is_error) if(it->is_error)
{ {
sent_type = MSG_SYSTEM; MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, MSG_SYSTEM, FmtWithLint(scratch.arena, "ERROR, what you said is incorrect because: %.*s", it->speech_length, it->speech)));
current_string = FmtWithLint(scratch.arena, "ERROR, what you said is incorrect because: %.*s", it->speech_length, it->speech);
} }
else else
{ {
MessageType sent_type = -1;
MD_String8List cur_list = {0};
MD_String8 context_string = {0}; MD_String8 context_string = {0};
if(it->context.was_directed_at_somebody)
{ PushWithLint(scratch.arena, &cur_list, "{");
context_string = FmtWithLint(scratch.arena, "%s, talking to %s: ", characters[it->context.author_npc_kind].name, characters[it->context.directed_at_kind].name); if(!it->context.i_said_this)
}
else
{
context_string = FmtWithLint(scratch.arena, "%s: ", characters[it->context.author_npc_kind].name);
}
assert(context_string.size > 0);
if(it->context.eavesdropped_from_party)
{ {
context_string = FmtWithLint(scratch.arena, "While in the player's party, you hear: %.*s", MD_S8VArg(context_string)); PushWithLint(scratch.arena, &cur_list, "character: %s, ", characters[it->context.author_npc_kind].name);
} }
MD_String8 speech = MD_S8(it->speech, it->speech_length); MD_String8 speech = MD_S8(it->speech, it->speech_length);
if(it->context.author_npc_kind == NPC_Player) // add speech
{ {
MD_String8 splits[] = { MD_S8Lit("*") }; if(it->context.author_npc_kind == NPC_Player)
MD_String8List split_up_speech = MD_S8Split(scratch.arena, speech, ARRLEN(splits), splits); {
PushWithLint(scratch.arena, &cur_list, "speech: \"");
MD_String8 splits[] = { MD_S8Lit("*"), MD_S8Lit("\"") };
MD_String8List split_up_speech = MD_S8Split(scratch.arena, speech, ARRLEN(splits), splits);
MD_String8List to_join = {0}; // anything in between strings in splits[] should be replaced with arcane trickery,
int i = 0;
for(MD_String8Node * cur = split_up_speech.first; cur; cur = cur->next)
{
if(i % 2 == 0)
{
PushWithLint(scratch.arena, &cur_list, "%.*s", MD_S8VArg(cur->string));
}
else
{
PushWithLint(scratch.arena, &cur_list, "[The player is attempting to confuse the NPC with arcane trickery]");
}
i += 1;
}
PushWithLint(scratch.arena, &cur_list, "\", ");
// anything in between strings in splits[] should be replaced with arcane trickery, sent_type = MSG_USER;
int i = 0; }
for(MD_String8Node * cur = split_up_speech.first; cur; cur = cur->next) else
{ {
if(i % 2 == 0) PushWithLint(scratch.arena, &cur_list, "speech: \"%.*s\", ", MD_S8VArg(speech));
if(it->context.i_said_this)
{ {
MD_S8ListPush(scratch.arena, &to_join, cur->string); sent_type = MSG_ASSISTANT;
} }
else else
{ {
MD_S8ListPush(scratch.arena, &to_join, MD_S8Lit("[The player is attempting to confuse the NPC with arcane trickery]")); sent_type = MSG_USER;
} }
i += 1;
} }
MD_StringJoin join = { MD_S8Lit(""), MD_S8Lit(""), MD_S8Lit("") };
speech = MD_S8ListJoin(scratch.arena, to_join, &join);
sent_type = MSG_USER;
} }
else
// add thoughts
if(it->context.i_said_this)
{ {
sent_type = it->context.author_npc_kind == e->npc_kind ? MSG_ASSISTANT : MSG_USER; PushWithLint(scratch.arena, &cur_list, "thoughts: \"%.*s\", ", MD_S8VArg(MD_S8(it->internal_monologue, it->internal_monologue_length)));
} }
// add action
PushWithLint(scratch.arena, &cur_list, "action: %s, ", actions[it->action_taken].name);
if(actions[it->action_taken].takes_argument) if(actions[it->action_taken].takes_argument)
{ {
if(it->action_taken == ACT_give_item) if(it->action_taken == ACT_give_item)
{ {
current_string = FmtWithLint(scratch.arena, "%.*s ACT_%s(ITEM_%s) \"%.*s\"", MD_S8VArg(context_string), actions[it->action_taken].name, items[it->action_argument.item_to_give].enum_name, it->speech_length, it->speech); PushWithLint(scratch.arena, &cur_list, "action_arg: %s, ", items[it->action_argument.item_to_give].enum_name);
} }
else else
{ {
assert(false); // don't know how to serialize this action with argument into text assert(false); // don't know how to serialize this action with argument into text
} }
} }
else
{
current_string = FmtWithLint(scratch.arena, "%.*s ACT_%s \"%.*s\"", MD_S8VArg(context_string), actions[it->action_taken].name, it->speech_length, it->speech);
}
if(it->context.i_said_this) PushWithLint(scratch.arena, &cur_list, "}");
{
current_string = FmtWithLint(scratch.arena, "%.*s [%.*s]", MD_S8VArg(current_string), it->internal_monologue_length, it->internal_monologue);
}
}
assert(sent_type != -1); assert(sent_type != -1);
assert(current_string.size > 0); MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, MSG_SYSTEM, MD_S8ListJoin(scratch.arena, cur_list, &(MD_StringJoin){0})));
}
MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, sent_type, current_string));
} }
MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, MSG_SYSTEM, MD_S8ListJoin(scratch.arena, first_system_string, &(MD_StringJoin){0}))); MD_S8ListPush(scratch.arena, &list, make_json_node(scratch.arena, MSG_SYSTEM, MD_S8ListJoin(scratch.arena, first_system_string, &(MD_StringJoin){0})));
@ -620,11 +619,11 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e)
{ {
if (i == available.cur_index - 1) if (i == available.cur_index - 1)
{ {
PushWithLint(scratch.arena, &latest_state, "ACT_%s", actions[*it].name); PushWithLint(scratch.arena, &latest_state, "%s", actions[*it].name);
} }
else else
{ {
PushWithLint(scratch.arena, &latest_state, "ACT_%s, ", actions[*it].name); PushWithLint(scratch.arena, &latest_state, "%s, ", actions[*it].name);
} }
} }
PushWithLint(scratch.arena, &latest_state, "]"); PushWithLint(scratch.arena, &latest_state, "]");
@ -666,6 +665,12 @@ MD_String8 generate_chatgpt_prompt(MD_Arena *arena, Entity *e)
return to_return; return to_return;
} }
MD_String8 get_field(MD_Node *parent, MD_String8 name)
{
return MD_ChildFromString(parent, name, 0)->first_child->string;
}
// if returned string has size greater than 0, it's the error message. Allocated // if returned string has size greater than 0, it's the error message. Allocated
// on arena passed into it // on arena passed into it
MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentence, Action *out) MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentence, Action *out)
@ -676,166 +681,104 @@ MD_String8 parse_chatgpt_response(MD_Arena *arena, Entity *e, MD_String8 sentenc
*out = (Action) { 0 }; *out = (Action) { 0 };
MD_String8 action_prefix = MD_S8Lit("ACT_"); MD_ParseResult result = MD_ParseWholeString(scratch.arena, MD_S8Lit("chat_message"), sentence);
MD_u64 act_pos = MD_S8FindSubstring(sentence, action_prefix, 0, 0); if(result.errors.node_count > 0)
if(act_pos == sentence.size)
{ {
error_message = FmtWithLint(arena, "Expected an `ACT_` somewhere in your sentence, followed by the action you want to perform, but couldn't find one"); MD_Message *cur = result.errors.first;
goto endofparsing; MD_CodeLoc loc = MD_CodeLocFromNode(cur->node);
error_message = FmtWithLint(arena, "Parse Error on column %d: %.*s", loc.column, MD_S8VArg(cur->string));
} }
MD_u64 beginning_of_action = act_pos + action_prefix.size; MD_Node *message_obj = result.node->first_child;
MD_u64 parenth = MD_S8FindSubstring(sentence, MD_S8Lit("("), beginning_of_action, 0); MD_String8 action_str = {0};
MD_u64 space = MD_S8FindSubstring(sentence, MD_S8Lit(" "), beginning_of_action, 0); MD_String8 speech_str = {0};
MD_String8 thoughts_str = {0};
MD_u64 end_of_action = parenth < space ? parenth : space; MD_String8 action_arg_str = {0};
if(end_of_action == sentence.size) if(error_message.size == 0)
{ {
error_message = FmtWithLint(arena, "'%.*s' prefix doesn't end with a ' ' or a '(', like how 'ACT_none ' or 'ACT_give_item(ITEM_sandwich) does.", MD_S8VArg(action_prefix)); action_str = get_field(message_obj, MD_S8Lit("action"));
goto endofparsing; speech_str = get_field(message_obj, MD_S8Lit("speech"));
thoughts_str = get_field(message_obj, MD_S8Lit("thoughts"));
action_arg_str = get_field(message_obj, MD_S8Lit("action_arg"));
} }
MD_String8 given_action_string = MD_S8Substring(sentence, beginning_of_action, end_of_action);
AvailableActions available = { 0 }; if(error_message.size == 0 && action_str.size == 0)
fill_available_actions(e, &available);
bool found_action = false;
MD_String8List given_action_strings = {0};
BUFF_ITER(ActionKind, &available)
{ {
MD_String8 action_str = MD_S8CString(actions[*it].name); error_message = MD_S8Lit("Expected field named `action` in message");
MD_S8ListPush(scratch.arena, &given_action_strings, action_str);
if(MD_S8Match(action_str, given_action_string, 0))
{
found_action = true;
out->kind = *it;
}
} }
if(error_message.size == 0 && speech_str.size >= MAX_SENTENCE_LENGTH)
if(!found_action)
{ {
if(MD_S8Match(given_action_string, MD_S8Lit("ACT_joins_player"), 0) && e->standing == STANDING_JOINED) error_message = FmtWithLint(arena, "Speech string provided is too big, maximum bytes is %d", MAX_SENTENCE_LENGTH);
{ }
error_message = MD_S8Lit("Cannot join the player again when you've already joined them"); if(error_message.size == 0 && thoughts_str.size >= MAX_SENTENCE_LENGTH)
} {
else error_message = FmtWithLint(arena, "Thoughts string provided is too big, maximum bytes is %d", MAX_SENTENCE_LENGTH);
{
MD_StringJoin join = {.pre = MD_S8Lit(""), .mid = MD_S8Lit(", "), .post = MD_S8Lit("")};
MD_String8 possible_actions_str = MD_S8ListJoin(scratch.arena, given_action_strings, &join);
error_message = FmtWithLint(arena, "ActionKind string given is '%.*s', but available actions are: [%.*s]", MD_S8VArg(given_action_string), MD_S8VArg(possible_actions_str));
}
goto endofparsing;
} }
MD_u64 start_looking_for_quote = end_of_action; if(error_message.size == 0)
{
memcpy(out->speech, speech_str.str, speech_str.size);
out->speech_length = (int)speech_str.size;
memcpy(out->internal_monologue, thoughts_str.str, thoughts_str.size);
out->internal_monologue_length = (int)thoughts_str.size;
}
if(actions[out->kind].takes_argument) if(error_message.size == 0)
{ {
if(end_of_action >= sentence.size) AvailableActions available = {0};
fill_available_actions(e, &available);
MD_String8List action_strings = {0};
bool found_action = false;
BUFF_ITER(ActionKind, &available)
{ {
error_message = FmtWithLint(arena, "Expected '(' after the given action '%.*s%.*s' which takes an argument, but sentence ended prematurely", MD_S8VArg(action_prefix), MD_S8VArg(MD_S8CString(actions[out->kind].name))); MD_String8 cur_action_string = MD_S8CString(actions[*it].name);
goto endofparsing; MD_S8ListPush(scratch.arena, &action_strings, cur_action_string);
} if(MD_S8Match(cur_action_string, action_str, 0))
char should_be_paren = sentence.str[end_of_action]; {
if(should_be_paren != '(') out->kind = *it;
{ found_action = true;
error_message = FmtWithLint(arena, "Expected '(' after the given action '%.*s%.*s' which takes an argument, but found character '%c'", MD_S8VArg(action_prefix), MD_S8VArg(MD_S8CString(actions[out->kind].name)), should_be_paren); }
goto endofparsing;
} }
MD_u64 beginning_of_arg = end_of_action;
MD_u64 end_of_arg = MD_S8FindSubstring(sentence, MD_S8Lit(")"), beginning_of_arg, 0); if(!found_action)
if(end_of_arg == sentence.size)
{ {
error_message = FmtWithLint(arena, "Expected ')' to close the action string's argument, but couldn't find one"); MD_String8 list_of_actions = MD_S8ListJoin(scratch.arena, action_strings, &(MD_StringJoin){.mid = MD_S8Lit(", ")});
goto endofparsing; error_message = FmtWithLint(arena, "Couldn't find action you can perform for provided string `%.*s`. Your available actions: [%.*s]", MD_S8VArg(action_str), MD_S8VArg(list_of_actions));
} }
}
MD_String8 argument = MD_S8Substring(sentence, beginning_of_arg, end_of_arg); if(error_message.size == 0)
start_looking_for_quote = end_of_arg + 1; {
if(actions[out->kind].takes_argument)
if(out->kind == ACT_give_item)
{ {
MD_String8 item_prefix = MD_S8Lit("ITEM_"); MD_String8List item_enum_names = {0};
MD_u64 item_prefix_begin = MD_S8FindSubstring(argument, item_prefix, 0, 0); if(out->kind == ACT_give_item)
if(item_prefix_begin == argument.size)
{ {
error_message = FmtWithLint(arena, "Expected prefix 'ITEM_' before the give_item action, but found '%.*s' instead", MD_S8VArg(argument)); bool found_item = false;
goto endofparsing; BUFF_ITER(ItemKind, &e->held_items)
}
MD_u64 item_name_begin = item_prefix_begin + item_prefix.size;
MD_u64 item_name_end = argument.size;
MD_String8 item_name = MD_S8Substring(argument, item_name_begin, item_name_end);
bool item_found = false;
MD_String8List possible_item_strings = {0};
BUFF_ITER(ItemKind, &e->held_items)
{
MD_String8 item_str = MD_S8CString(items[*it].enum_name);
MD_S8ListPush(scratch.arena, &possible_item_strings, item_str);
if(MD_S8Match(item_str, item_name, 0))
{ {
item_found = true; MD_String8 cur_item_string = MD_S8CString(items[*it].enum_name);
out->argument.item_to_give = *it; MD_S8ListPush(scratch.arena, &item_enum_names, cur_item_string);
if(MD_S8Match(cur_item_string, action_arg_str, 0))
{
found_item = true;
out->argument.item_to_give = *it;
}
}
if(!found_item)
{
MD_String8 list_of_items = MD_S8ListJoin(scratch.arena, item_enum_names, &(MD_StringJoin){.mid = MD_S8Lit(", ")});
error_message = FmtWithLint(arena, "Couldn't find item you said to give in action_arg, `%.*s`, the items you have in your inventory to give are: [%.*s]", MD_S8VArg(action_arg_str), MD_S8VArg(list_of_items));
} }
} }
else
if(!item_found)
{ {
MD_StringJoin join = {.pre = MD_S8Lit(""), .mid = MD_S8Lit(", "), .post = MD_S8Lit("")}; assert(false); // don't know how to parse the argument string for this kind of action...
MD_String8 possible_items_str = MD_S8ListJoin(scratch.arena, possible_item_strings, &join);
error_message = FmtWithLint(arena, "Item string given is '%.*s', but available items to give are: [%.*s]", MD_S8VArg(item_name), MD_S8VArg(possible_items_str));
goto endofparsing;
} }
} }
else
{
assert(false); // if action takes an argument but we don't handle it, this should be a terrible crash
}
}
if(start_looking_for_quote >= sentence.size)
{
error_message = FmtWithLint(arena, "Wanted to start looking for quote for NPC speech, but sentence ended prematurely");
goto endofparsing;
} }
MD_u64 beginning_of_speech = MD_S8FindSubstring(sentence, MD_S8Lit("\""), 0, 0);
MD_u64 end_of_speech = MD_S8FindSubstring(sentence, MD_S8Lit("\""), beginning_of_speech + 1, 0);
if(beginning_of_speech == sentence.size || end_of_speech == sentence.size)
{
error_message = FmtWithLint(arena, "Expected dialog enclosed by two quotes (i.e \"My name is greg\") after the action, but couldn't find anything!");
goto endofparsing;
}
MD_String8 speech = MD_S8Substring(sentence, beginning_of_speech + 1, end_of_speech);
if(speech.size >= ARRLEN(out->speech))
{
error_message = FmtWithLint(arena, "The speech given is %llu bytes big, but the maximum allowed is %llu bytes.", speech.size, ARRLEN(out->speech));
goto endofparsing;
}
memcpy(out->speech, speech.str, speech.size);
out->speech_length = (int)speech.size;
MD_u64 beginning_of_monologue = MD_S8FindSubstring(sentence, MD_S8Lit("["), end_of_speech, 0);
MD_u64 end_of_monologue = MD_S8FindSubstring(sentence, MD_S8Lit("]"), beginning_of_monologue, 0);
if(beginning_of_monologue == sentence.size || end_of_monologue == sentence.size)
{
error_message = FmtWithLint(arena, "Expected an internal monologue for your character enclosed by '[' and ']' after the speech in quotes, but couldn't find anything!");
goto endofparsing;
}
MD_String8 monologue = MD_S8Substring(sentence, beginning_of_monologue + 1, end_of_monologue);
memcpy(out->internal_monologue, monologue.str, monologue.size);
out->internal_monologue_length = (int)monologue.size;
endofparsing:
MD_ReleaseScratch(scratch); MD_ReleaseScratch(scratch);
return error_message; return error_message;
} }

Loading…
Cancel
Save