jp.keijiro.ai.assistant.extensions 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/.attestation.p7m
CHANGED
|
Binary file
|
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
|
|
6
6
|
and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [1.0.1] - 2026-04-24
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- Reimplemented Conversation Extractor as a Unity Editor window that reads `Logs/relay.txt` and lets you review, copy, and save extracted conversations.
|
|
13
|
+
|
|
8
14
|
## [1.0.0] - 2025-04-23
|
|
9
15
|
|
|
10
16
|
- Initial version.
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
using System;
|
|
2
|
+
using System.Collections.Generic;
|
|
3
|
+
using System.IO;
|
|
4
|
+
using System.Text;
|
|
5
|
+
using System.Text.RegularExpressions;
|
|
6
|
+
using UnityEditor;
|
|
7
|
+
using UnityEngine;
|
|
8
|
+
|
|
9
|
+
namespace AIAssistantExtensions {
|
|
10
|
+
|
|
11
|
+
class ConversationExtractorWindow : EditorWindow
|
|
12
|
+
{
|
|
13
|
+
const string RelayLogRelativePath = "Logs/relay.txt";
|
|
14
|
+
|
|
15
|
+
static readonly Regex MessageRegex = new(
|
|
16
|
+
"\"\\$type\":\"(?<type>[^\"]+)\".*?\"message_id\":\"(?<messageId>[^\"]+)\".*?(?:\"last_message\":(?<lastMessage>true|false).*?)?\"markdown\":\"(?<markdown>(?:\\\\.|[^\"\\\\])*)\"",
|
|
17
|
+
RegexOptions.Compiled);
|
|
18
|
+
|
|
19
|
+
Vector2 _scroll;
|
|
20
|
+
string _markdown;
|
|
21
|
+
string _status;
|
|
22
|
+
bool _includeEmpty;
|
|
23
|
+
|
|
24
|
+
[MenuItem("Window/AI/Conversation Extractor")]
|
|
25
|
+
static void Open()
|
|
26
|
+
{
|
|
27
|
+
var window = GetWindow<ConversationExtractorWindow>();
|
|
28
|
+
window.titleContent = new GUIContent("Conversation Extractor");
|
|
29
|
+
window.minSize = new Vector2(480, 320);
|
|
30
|
+
window.Extract();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
void OnGUI()
|
|
34
|
+
{
|
|
35
|
+
using (new EditorGUILayout.HorizontalScope(EditorStyles.toolbar))
|
|
36
|
+
{
|
|
37
|
+
var includeEmpty = GUILayout.Toggle(_includeEmpty, "Include Empty", EditorStyles.toolbarButton);
|
|
38
|
+
if (includeEmpty != _includeEmpty) _includeEmpty = includeEmpty;
|
|
39
|
+
|
|
40
|
+
GUILayout.FlexibleSpace();
|
|
41
|
+
|
|
42
|
+
if (GUILayout.Button("Extract", EditorStyles.toolbarButton))
|
|
43
|
+
Extract();
|
|
44
|
+
if (GUILayout.Button("Copy", EditorStyles.toolbarButton))
|
|
45
|
+
CopyToClipboard();
|
|
46
|
+
if (GUILayout.Button("Save", EditorStyles.toolbarButton))
|
|
47
|
+
SaveMarkdown();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!string.IsNullOrEmpty(_status))
|
|
51
|
+
EditorGUILayout.HelpBox(_status, MessageType.Info);
|
|
52
|
+
|
|
53
|
+
using var scroll = new EditorGUILayout.ScrollViewScope(_scroll);
|
|
54
|
+
_scroll = scroll.scrollPosition;
|
|
55
|
+
EditorGUILayout.TextArea(_markdown ?? "", GUILayout.ExpandHeight(true));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
void Extract()
|
|
59
|
+
{
|
|
60
|
+
var path = GetRelayLogPath();
|
|
61
|
+
if (!File.Exists(path))
|
|
62
|
+
{
|
|
63
|
+
_markdown = "";
|
|
64
|
+
_status = $"Relay log not found: {path}";
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
var pairs = ExtractPairs(path);
|
|
69
|
+
_markdown = BuildMarkdown(pairs, _includeEmpty);
|
|
70
|
+
_status = $"Extracted {pairs.Count} conversation pair(s) from {RelayLogRelativePath}.";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
void CopyToClipboard()
|
|
74
|
+
{
|
|
75
|
+
EditorGUIUtility.systemCopyBuffer = _markdown ?? "";
|
|
76
|
+
_status = "Copied extracted markdown to the clipboard.";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
void SaveMarkdown()
|
|
80
|
+
{
|
|
81
|
+
var projectRoot = GetProjectRootPath();
|
|
82
|
+
var defaultPath = Path.Combine(projectRoot, "Logs", "conversations.md");
|
|
83
|
+
var path = EditorUtility.SaveFilePanel("Save extracted conversations", Path.GetDirectoryName(defaultPath), Path.GetFileName(defaultPath), "md");
|
|
84
|
+
if (string.IsNullOrEmpty(path)) return;
|
|
85
|
+
|
|
86
|
+
File.WriteAllText(path, _markdown ?? "", Encoding.UTF8);
|
|
87
|
+
_status = $"Saved extracted markdown to {path}";
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
static List<ConversationPair> ExtractPairs(string path)
|
|
91
|
+
{
|
|
92
|
+
var pairs = new List<ConversationPair>();
|
|
93
|
+
ConversationPair currentPair = null;
|
|
94
|
+
string activeResponseId = null;
|
|
95
|
+
|
|
96
|
+
var lines = File.ReadAllLines(path, Encoding.UTF8);
|
|
97
|
+
for (var i = 0; i < lines.Length; i++)
|
|
98
|
+
{
|
|
99
|
+
var parsed = ParseLine(lines[i]);
|
|
100
|
+
if (parsed == null) continue;
|
|
101
|
+
|
|
102
|
+
if (parsed.Type == "CHAT_ACKNOWLEDGMENT_V1" && !string.IsNullOrWhiteSpace(parsed.Markdown))
|
|
103
|
+
{
|
|
104
|
+
if (currentPair != null && currentPair.ResponseParts.Count > 0)
|
|
105
|
+
pairs.Add(currentPair);
|
|
106
|
+
|
|
107
|
+
currentPair = new ConversationPair(parsed.Markdown.Trim(), i + 1, parsed.MessageId);
|
|
108
|
+
activeResponseId = null;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (parsed.Type != "CHAT_RESPONSE_V1" || currentPair == null) continue;
|
|
113
|
+
if (string.IsNullOrEmpty(parsed.Markdown) || parsed.Markdown.StartsWith("<")) continue;
|
|
114
|
+
|
|
115
|
+
if (activeResponseId == null)
|
|
116
|
+
{
|
|
117
|
+
activeResponseId = parsed.MessageId;
|
|
118
|
+
currentPair.ResponseMessageId = activeResponseId;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (parsed.MessageId == activeResponseId)
|
|
122
|
+
{
|
|
123
|
+
currentPair.ResponseParts.Add(parsed.Markdown);
|
|
124
|
+
currentPair.ResponseLines.Add(i + 1);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (parsed.LastMessage && parsed.MessageId == activeResponseId)
|
|
128
|
+
{
|
|
129
|
+
pairs.Add(currentPair);
|
|
130
|
+
currentPair = null;
|
|
131
|
+
activeResponseId = null;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (currentPair != null)
|
|
136
|
+
pairs.Add(currentPair);
|
|
137
|
+
|
|
138
|
+
return pairs;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
static ParsedLine ParseLine(string line)
|
|
142
|
+
{
|
|
143
|
+
var match = MessageRegex.Match(line);
|
|
144
|
+
if (!match.Success) return null;
|
|
145
|
+
|
|
146
|
+
return new ParsedLine
|
|
147
|
+
(
|
|
148
|
+
match.Groups["type"].Value,
|
|
149
|
+
match.Groups["messageId"].Value,
|
|
150
|
+
match.Groups["lastMessage"].Value == "true",
|
|
151
|
+
DecodeMarkdown(match.Groups["markdown"].Value)
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
static string DecodeMarkdown(string value)
|
|
156
|
+
=> JsonUtility.FromJson<JsonStringWrapper>("{\"value\":\"" + value + "\"}")?.value ?? "";
|
|
157
|
+
|
|
158
|
+
static string BuildMarkdown(List<ConversationPair> pairs, bool includeEmpty)
|
|
159
|
+
{
|
|
160
|
+
var builder = new StringBuilder();
|
|
161
|
+
|
|
162
|
+
foreach (var pair in pairs)
|
|
163
|
+
{
|
|
164
|
+
var response = pair.Response.Trim();
|
|
165
|
+
if (!includeEmpty && string.IsNullOrEmpty(response)) continue;
|
|
166
|
+
|
|
167
|
+
if (builder.Length > 0)
|
|
168
|
+
builder.Append("\n\n");
|
|
169
|
+
|
|
170
|
+
builder.Append("## User\n\n");
|
|
171
|
+
builder.Append(pair.Prompt);
|
|
172
|
+
builder.Append("\n\n## Assistant\n\n");
|
|
173
|
+
builder.Append(string.IsNullOrEmpty(response) ? "(No extracted response)" : response);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return builder.ToString();
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
static string GetProjectRootPath()
|
|
180
|
+
=> Directory.GetParent(Application.dataPath)?.FullName ?? Directory.GetCurrentDirectory();
|
|
181
|
+
|
|
182
|
+
static string GetRelayLogPath()
|
|
183
|
+
=> Path.Combine(GetProjectRootPath(), RelayLogRelativePath);
|
|
184
|
+
|
|
185
|
+
[Serializable]
|
|
186
|
+
class JsonStringWrapper
|
|
187
|
+
{
|
|
188
|
+
public string value = "";
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
class ConversationPair
|
|
192
|
+
{
|
|
193
|
+
public readonly string Prompt;
|
|
194
|
+
public readonly int PromptLine;
|
|
195
|
+
public readonly string PromptMessageId;
|
|
196
|
+
public readonly List<string> ResponseParts = new();
|
|
197
|
+
public readonly List<int> ResponseLines = new();
|
|
198
|
+
public string ResponseMessageId;
|
|
199
|
+
|
|
200
|
+
public string Response => string.Concat(ResponseParts);
|
|
201
|
+
|
|
202
|
+
public ConversationPair(string prompt, int promptLine, string promptMessageId)
|
|
203
|
+
{
|
|
204
|
+
Prompt = prompt;
|
|
205
|
+
PromptLine = promptLine;
|
|
206
|
+
PromptMessageId = promptMessageId;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
class ParsedLine
|
|
211
|
+
{
|
|
212
|
+
public readonly string Type;
|
|
213
|
+
public readonly string MessageId;
|
|
214
|
+
public readonly bool LastMessage;
|
|
215
|
+
public readonly string Markdown;
|
|
216
|
+
|
|
217
|
+
public ParsedLine(string type, string messageId, bool lastMessage, string markdown)
|
|
218
|
+
{
|
|
219
|
+
Type = type;
|
|
220
|
+
MessageId = messageId;
|
|
221
|
+
LastMessage = lastMessage;
|
|
222
|
+
Markdown = markdown;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
} // namespace AIAssistantExtensions
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jp.keijiro.ai.assistant.extensions",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"displayName": "AI Assistant Extensions",
|
|
5
5
|
"description": "Custom Unity package that provides extensions for Unity AI Assistant.",
|
|
6
6
|
"unity": "6000.0",
|
|
@@ -13,11 +13,11 @@
|
|
|
13
13
|
"licensesUrl": "https://github.com/keijiro/AIA-Extensions/blob/master/LICENSE",
|
|
14
14
|
"license": "Unlicense",
|
|
15
15
|
"_upm": {
|
|
16
|
-
"changelog": "-
|
|
16
|
+
"changelog": "- Reimplemented Conversation Extractor as a Unity Editor window that reads `Logs/relay.txt` and lets you review, copy, and save extracted conversations."
|
|
17
17
|
},
|
|
18
18
|
"repository": {
|
|
19
19
|
"url": "git@github.com:keijiro/AIA-Extensions.git",
|
|
20
20
|
"type": "git",
|
|
21
|
-
"revision": "
|
|
21
|
+
"revision": "56ca9552dd1911766223a864cba2a4267e23cde6"
|
|
22
22
|
}
|
|
23
23
|
}
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
from __future__ import annotations
|
|
3
|
-
|
|
4
|
-
import argparse
|
|
5
|
-
import json
|
|
6
|
-
import re
|
|
7
|
-
from dataclasses import dataclass, field
|
|
8
|
-
from pathlib import Path
|
|
9
|
-
from typing import Optional
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
MESSAGE_RE = re.compile(
|
|
13
|
-
r'"\$type":"(?P<type>[^"]+)".*?'
|
|
14
|
-
r'"message_id":"(?P<message_id>[^"]+)".*?'
|
|
15
|
-
r'(?:"last_message":(?P<last_message>true|false).*?)?'
|
|
16
|
-
r'"markdown":"(?P<markdown>(?:\\.|[^"\\])*)"',
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
@dataclass
|
|
21
|
-
class ConversationPair:
|
|
22
|
-
prompt: str
|
|
23
|
-
prompt_line: int
|
|
24
|
-
prompt_message_id: str
|
|
25
|
-
response_parts: list[str] = field(default_factory=list)
|
|
26
|
-
response_lines: list[int] = field(default_factory=list)
|
|
27
|
-
response_message_id: Optional[str] = None
|
|
28
|
-
|
|
29
|
-
@property
|
|
30
|
-
def response(self) -> str:
|
|
31
|
-
return "".join(self.response_parts).strip()
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def decode_markdown(value: str) -> str:
|
|
35
|
-
return json.loads(f'"{value}"')
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def parse_line(line: str):
|
|
39
|
-
match = MESSAGE_RE.search(line)
|
|
40
|
-
if not match:
|
|
41
|
-
return None
|
|
42
|
-
|
|
43
|
-
groups = match.groupdict()
|
|
44
|
-
return {
|
|
45
|
-
"type": groups["type"],
|
|
46
|
-
"message_id": groups["message_id"],
|
|
47
|
-
"last_message": groups["last_message"] == "true",
|
|
48
|
-
"markdown": decode_markdown(groups["markdown"]),
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
def extract_pairs(path: Path) -> list[ConversationPair]:
|
|
53
|
-
pairs: list[ConversationPair] = []
|
|
54
|
-
current_pair: Optional[ConversationPair] = None
|
|
55
|
-
active_response_id: Optional[str] = None
|
|
56
|
-
|
|
57
|
-
for line_number, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
|
|
58
|
-
parsed = parse_line(raw_line)
|
|
59
|
-
if not parsed:
|
|
60
|
-
continue
|
|
61
|
-
|
|
62
|
-
message_type = parsed["type"]
|
|
63
|
-
markdown = parsed["markdown"]
|
|
64
|
-
|
|
65
|
-
if message_type == "CHAT_ACKNOWLEDGMENT_V1" and markdown:
|
|
66
|
-
if current_pair and current_pair.response_parts:
|
|
67
|
-
pairs.append(current_pair)
|
|
68
|
-
|
|
69
|
-
current_pair = ConversationPair(
|
|
70
|
-
prompt=markdown.strip(),
|
|
71
|
-
prompt_line=line_number,
|
|
72
|
-
prompt_message_id=parsed["message_id"],
|
|
73
|
-
)
|
|
74
|
-
active_response_id = None
|
|
75
|
-
continue
|
|
76
|
-
|
|
77
|
-
if message_type != "CHAT_RESPONSE_V1" or current_pair is None:
|
|
78
|
-
continue
|
|
79
|
-
|
|
80
|
-
if markdown and not markdown.startswith("<"):
|
|
81
|
-
if active_response_id is None:
|
|
82
|
-
active_response_id = parsed["message_id"]
|
|
83
|
-
current_pair.response_message_id = active_response_id
|
|
84
|
-
|
|
85
|
-
if parsed["message_id"] == active_response_id:
|
|
86
|
-
current_pair.response_parts.append(markdown)
|
|
87
|
-
current_pair.response_lines.append(line_number)
|
|
88
|
-
|
|
89
|
-
if parsed["last_message"] and active_response_id == parsed["message_id"]:
|
|
90
|
-
pairs.append(current_pair)
|
|
91
|
-
current_pair = None
|
|
92
|
-
active_response_id = None
|
|
93
|
-
|
|
94
|
-
if current_pair:
|
|
95
|
-
pairs.append(current_pair)
|
|
96
|
-
|
|
97
|
-
return pairs
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def build_markdown(pairs: list[ConversationPair], include_empty: bool) -> str:
|
|
101
|
-
sections = []
|
|
102
|
-
|
|
103
|
-
for pair in pairs:
|
|
104
|
-
if not include_empty and not pair.response:
|
|
105
|
-
continue
|
|
106
|
-
|
|
107
|
-
response = pair.response or "(No extracted response)"
|
|
108
|
-
sections.append(f"## User\n\n{pair.prompt}\n\n## Assistant\n\n{response}")
|
|
109
|
-
|
|
110
|
-
return "\n\n".join(sections)
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def main():
|
|
114
|
-
parser = argparse.ArgumentParser(
|
|
115
|
-
description="Extract user prompts and natural-language assistant replies from Unity AI relay logs."
|
|
116
|
-
)
|
|
117
|
-
parser.add_argument("logfile", nargs="?", default="relay.txt", help="Path to relay log file")
|
|
118
|
-
parser.add_argument("--include-empty", action="store_true", help="Include prompts without extracted replies")
|
|
119
|
-
args = parser.parse_args()
|
|
120
|
-
|
|
121
|
-
path = Path(args.logfile)
|
|
122
|
-
pairs = extract_pairs(path)
|
|
123
|
-
print(build_markdown(pairs, args.include_empty))
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
if __name__ == "__main__":
|
|
127
|
-
main()
|