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
@@ -0,0 +1,2 @@
1
+ fileFormatVersion: 2
2
+ guid: 17b29e54e64e4ff0b5f6fd8fd7b4dc81
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jp.keijiro.ai.assistant.extensions",
3
- "version": "1.0.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": "- Initial release."
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": "5972c2dd0d5b6a7ad01c2de12f17396f77b1cfc7"
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()