Skip to content

Commit 26fb8ee

Browse files
committed
wip
1 parent 6e2b056 commit 26fb8ee

File tree

9 files changed

+1551
-6
lines changed

9 files changed

+1551
-6
lines changed

webapp/src/components/MessageList.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ const MessageContent = styled.div`
5959
cursor: pointer;
6060
user-select: none;
6161
display: inline-block;
62-
padding: 2px 8px;
6362
margin: 2px;
6463
border-radius: 4px;
6564
background-color: var(--theme-surface);

webapp/src/store/slices/messageSlice.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ const sanitizeHtmlContent = (content: string): string => {
2020
console.debug(` Sanitizing HTML content`);
2121
return DOMPurify.sanitize(content, {
2222
ALLOWED_TAGS: ['div', 'span', 'p', 'br', 'b', 'i', 'em', 'strong', 'a', 'ul', 'ol', 'li', 'code', 'pre', 'table', 'tr', 'td', 'th', 'thead', 'tbody',
23-
'button', 'input', 'label', 'select', 'option', 'textarea', 'code', 'pre', 'div', 'section'],
23+
'button', 'input', 'label', 'select', 'option', 'textarea', 'code', 'pre', 'div', 'section', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'img', 'figure', 'figcaption',],
2424
ALLOWED_ATTR: ['class', 'href', 'target', 'data-tab', 'data-for-tab', 'style', 'type', 'value', 'id', 'name',
2525
'data-message-id', 'data-id', 'data-message-action', 'data-action', 'data-ref-id', 'data-version', 'role', 'message-id'],
2626
});
Lines changed: 348 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,348 @@
1+
package com.simiacryptus.skyenet.apps.general
2+
3+
import com.simiacryptus.jopenai.API
4+
import com.simiacryptus.jopenai.describe.JsonDescriber
5+
import com.simiacryptus.jopenai.models.ChatModel
6+
import com.simiacryptus.jopenai.models.OpenAIModels
7+
import com.simiacryptus.jopenai.util.GPT4Tokenizer
8+
import com.simiacryptus.skyenet.TabbedDisplay
9+
import com.simiacryptus.skyenet.apps.general.OutlineManager.NodeList
10+
import com.simiacryptus.skyenet.core.actors.ActorSystem
11+
import com.simiacryptus.skyenet.core.actors.LargeOutputActor
12+
import com.simiacryptus.skyenet.core.actors.ParsedActor
13+
import com.simiacryptus.skyenet.core.platform.Session
14+
import com.simiacryptus.skyenet.core.platform.model.StorageInterface
15+
import com.simiacryptus.skyenet.core.platform.model.User
16+
import com.simiacryptus.skyenet.util.MarkdownUtil.renderMarkdown
17+
import com.simiacryptus.skyenet.util.TensorflowProjector
18+
import com.simiacryptus.skyenet.webui.application.ApplicationInterface
19+
import com.simiacryptus.skyenet.webui.application.ApplicationServer
20+
import com.simiacryptus.skyenet.webui.session.SessionTask
21+
import com.simiacryptus.util.JsonUtil
22+
import org.intellij.lang.annotations.Language
23+
import org.slf4j.LoggerFactory
24+
import java.util.concurrent.atomic.AtomicInteger
25+
26+
open class OutlineApp(
27+
applicationName: String = "Outline Expansion Concept Map v1.1",
28+
val domainName: String,
29+
settings: Settings,
30+
) : ApplicationServer(
31+
applicationName = applicationName,
32+
path = "/idea_mapper",
33+
) {
34+
override val description: String
35+
@Language("HTML")
36+
get() = ("<div>" + renderMarkdown(
37+
"""
38+
The Outline Agent is an AI-powered tool for exploring concepts via outline creation and expansion.
39+
40+
Here's how it works:
41+
42+
1. **Generate Initial Outline**: Provide your main idea or topic, and the Outline Agent will create an initial outline.
43+
2. **Iterative Expansion**: The agent then expands on each section of your outline, adding depth and detail.
44+
3. **Construct Final Outline**: Once your outline is fully expanded, the agent can compile it into a single outline. This presents the information in a clear and concise manner, making it easy to review.
45+
4. **Visualize Embeddings**: Each section of your outline is represented as a vector in a high-dimensional space. The Outline Agent uses an Embedding Projector to visualize these vectors, allowing you to explore the relationships between different ideas and concepts.
46+
5. **Customizable Experience**: You can set the number of iterations and the model used for each to control the depth and price, making it possible to generate sizable outputs.
47+
48+
Start your journey into concept space today with the Outline Agent! 📝✨
49+
""".trimIndent()
50+
) + "</div>")
51+
52+
data class Settings(
53+
val models: List<ChatModel> = listOf(
54+
OpenAIModels.GPT4o,
55+
OpenAIModels.GPT4oMini
56+
),
57+
val parsingModel: ChatModel = OpenAIModels.GPT4oMini,
58+
val temperature: Double = 0.3,
59+
val minTokensForExpansion: Int = 16,
60+
val showProjector: Boolean = true,
61+
val writeFinalEssay: Boolean = false,
62+
val budget: Double = 2.0,
63+
)
64+
65+
override val settingsClass: Class<*> get() = Settings::class.java
66+
67+
@Suppress("UNCHECKED_CAST")
68+
override fun <T : Any> initSettings(session: Session): T? = Settings() as T
69+
70+
override fun userMessage(
71+
session: Session,
72+
user: User?,
73+
userMessage: String,
74+
ui: ApplicationInterface,
75+
api: API
76+
) {
77+
val settings = getSettings<Settings>(session, user)!!
78+
OutlineAgent(
79+
api = api,
80+
dataStorage = dataStorage,
81+
session = session,
82+
user = user,
83+
temperature = settings.temperature,
84+
models = settings.models.drop(1),
85+
firstLevelModel = settings.models.first(),
86+
parsingModel = settings.parsingModel,
87+
minSize = settings.minTokensForExpansion,
88+
writeFinalEssay = settings.writeFinalEssay,
89+
showProjector = settings.showProjector,
90+
userMessage = userMessage,
91+
ui = ui,
92+
).buildMap()
93+
}
94+
95+
}
96+
97+
class OutlineAgent(
98+
val api: API,
99+
dataStorage: StorageInterface,
100+
session: Session,
101+
user: User?,
102+
temperature: Double,
103+
val models: List<ChatModel>,
104+
val firstLevelModel: ChatModel,
105+
val parsingModel: ChatModel,
106+
private val minSize: Int,
107+
val writeFinalEssay: Boolean,
108+
val showProjector: Boolean,
109+
val userMessage: String,
110+
val ui: ApplicationInterface
111+
) : ActorSystem<OutlineActors.ActorType>(
112+
OutlineActors.actorMap(temperature, firstLevelModel, parsingModel).map { it.key.name to it.value }.toMap(),
113+
dataStorage,
114+
user,
115+
session
116+
) {
117+
private val tabbedDisplay = TabbedDisplay(ui.newTask())
118+
119+
init {
120+
require(models.isNotEmpty())
121+
}
122+
123+
@Suppress("UNCHECKED_CAST")
124+
private val initial get() = getActor(OutlineActors.ActorType.INITIAL) as ParsedActor<NodeList>
125+
private val summary get() = getActor(OutlineActors.ActorType.FINAL) as LargeOutputActor
126+
127+
@Suppress("UNCHECKED_CAST")
128+
private val expand get() = getActor(OutlineActors.ActorType.EXPAND) as ParsedActor<NodeList>
129+
private val activeThreadCounter = AtomicInteger(0)
130+
private val tokenizer = GPT4Tokenizer(false)
131+
132+
fun buildMap() {
133+
val message = ui.newTask(false)
134+
tabbedDisplay["Content"] = message.placeholder
135+
val outlineManager = try {
136+
message.echo(renderMarkdown(this.userMessage, ui = ui))
137+
val root = initial.answer(listOf(this.userMessage), api = api)
138+
message.add(renderMarkdown(root.text, ui = ui))
139+
message.verbose(JsonUtil.toJson(root.obj))
140+
message.complete()
141+
OutlineManager(OutlineManager.OutlinedText(root.text, root.obj))
142+
} catch (e: Exception) {
143+
message.error(ui, e)
144+
throw e
145+
}
146+
147+
if (models.isNotEmpty()) {
148+
processRecursive(outlineManager, outlineManager.rootNode, models, message)
149+
while (activeThreadCounter.get() == 0) Thread.sleep(100) // Wait for at least one thread to start
150+
while (activeThreadCounter.get() > 0) Thread.sleep(100) // Wait for all threads to finish
151+
}
152+
153+
val sessionDir = dataStorage.getSessionDir(user, session)
154+
sessionDir.resolve("nodes.json").writeText(JsonUtil.toJson(outlineManager.nodes))
155+
156+
val finalOutlineMessage = ui.newTask(false)
157+
tabbedDisplay["Outline"] = finalOutlineMessage.placeholder
158+
finalOutlineMessage.header("Final Outline")
159+
val finalOutline = outlineManager.buildFinalOutline()
160+
finalOutlineMessage.verbose(JsonUtil.toJson(finalOutline))
161+
val textOutline = finalOutline?.let { NodeList(it) }?.getTextOutline() ?: ""
162+
finalOutlineMessage.complete(renderMarkdown(textOutline, ui = ui))
163+
sessionDir.resolve("finalOutline.json").writeText(JsonUtil.toJson(finalOutline))
164+
sessionDir.resolve("textOutline.md").writeText(textOutline)
165+
166+
if (showProjector) {
167+
val projectorMessage = ui.newTask(false)
168+
tabbedDisplay["Projector"] = projectorMessage.placeholder
169+
projectorMessage.header("Embedding Projector")
170+
try {
171+
val response = TensorflowProjector(
172+
api = api,
173+
dataStorage = dataStorage,
174+
sessionID = session,
175+
session = ui,
176+
userId = user,
177+
).writeTensorflowEmbeddingProjectorHtml(
178+
*outlineManager.getLeafDescriptions(finalOutline?.let { NodeList(it) }!!).toTypedArray()
179+
)
180+
projectorMessage.complete(response)
181+
} catch (e: Exception) {
182+
log.warn("Error", e)
183+
projectorMessage.error(ui, e)
184+
}
185+
}
186+
187+
if (writeFinalEssay) {
188+
val finalRenderMessage = ui.newTask(false)
189+
tabbedDisplay["Final Essay"] = finalRenderMessage.placeholder
190+
finalRenderMessage.header("Final Render")
191+
try {
192+
val finalEssay = buildFinalEssay(finalOutline?.let { NodeList(it) }!!, outlineManager)
193+
sessionDir.resolve("finalEssay.md").writeText(finalEssay)
194+
finalRenderMessage.complete(renderMarkdown(finalEssay, ui = ui))
195+
} catch (e: Exception) {
196+
log.warn("Error", e)
197+
finalRenderMessage.error(ui, e)
198+
}
199+
}
200+
tabbedDisplay.update()
201+
}
202+
203+
private fun buildFinalEssay(
204+
nodeList: NodeList,
205+
manager: OutlineManager
206+
): String =
207+
if (tokenizer.estimateTokenCount(nodeList.getTextOutline()) > (summary.model.maxTotalTokens * 0.6).toInt()) {
208+
manager.expandNodes(nodeList)?.joinToString("\n") { buildFinalEssay(it, manager) } ?: ""
209+
} else {
210+
summary.answer(listOf(nodeList.getTextOutline()), api = api)
211+
}
212+
213+
private fun processRecursive(
214+
manager: OutlineManager,
215+
node: OutlineManager.OutlinedText,
216+
models: List<ChatModel>,
217+
task: SessionTask
218+
) {
219+
val tabbedDisplay = TabbedDisplay(task)
220+
val terminalNodeMap = node.outline.getTerminalNodeMap()
221+
if (terminalNodeMap.isEmpty()) {
222+
val errorMessage = "No terminal nodes: ${node.text}"
223+
log.warn(errorMessage)
224+
task.error(ui, RuntimeException(errorMessage))
225+
return
226+
}
227+
for ((item, childNode) in terminalNodeMap) {
228+
activeThreadCounter.incrementAndGet()
229+
val message = ui.newTask(false)
230+
tabbedDisplay[item] = message.placeholder
231+
pool.submit {
232+
try {
233+
val newNode = processNode(node, item, manager, message, models.first()) ?: return@submit
234+
synchronized(manager.expansionMap) {
235+
if (!manager.expansionMap.containsKey(childNode)) {
236+
manager.expansionMap[childNode] = newNode
237+
} else {
238+
val existingNode = manager.expansionMap[childNode]!!
239+
val errorMessage = "Conflict: ${existingNode} vs ${newNode}"
240+
log.warn(errorMessage)
241+
message.error(ui, RuntimeException(errorMessage))
242+
}
243+
}
244+
if (models.size > 1) processRecursive(manager, newNode, models.drop(1), message)
245+
} catch (e: Exception) {
246+
log.warn("Error in processRecursive", e)
247+
message.error(ui, e)
248+
} finally {
249+
activeThreadCounter.decrementAndGet()
250+
}
251+
}
252+
}
253+
task.complete()
254+
}
255+
256+
private fun processNode(
257+
parent: OutlineManager.OutlinedText,
258+
sectionName: String,
259+
outlineManager: OutlineManager,
260+
message: SessionTask,
261+
model: ChatModel,
262+
): OutlineManager.OutlinedText? {
263+
if (tokenizer.estimateTokenCount(parent.text) <= minSize) {
264+
log.debug("Skipping: ${parent.text}")
265+
return null
266+
}
267+
message.header("Expand $sectionName")
268+
val answer = expand.withModel(model).answer(listOf(this.userMessage, parent.text, sectionName), api = api)
269+
message.add(renderMarkdown(answer.text, ui = ui))
270+
message.verbose(JsonUtil.toJson(answer.obj), false)
271+
val newNode = OutlineManager.OutlinedText(answer.text, answer.obj)
272+
outlineManager.nodes.add(newNode)
273+
return newNode
274+
}
275+
276+
companion object {
277+
private val log = LoggerFactory.getLogger(OutlineAgent::class.java)
278+
}
279+
280+
}
281+
282+
interface OutlineActors {
283+
284+
enum class ActorType {
285+
INITIAL,
286+
EXPAND,
287+
FINAL,
288+
}
289+
290+
companion object {
291+
292+
val log = LoggerFactory.getLogger(OutlineActors::class.java)
293+
294+
fun actorMap(temperature: Double, firstLevelModel: ChatModel, parsingModel: ChatModel) = mapOf(
295+
ActorType.INITIAL to initialAuthor(temperature, firstLevelModel, parsingModel),
296+
ActorType.EXPAND to expansionAuthor(temperature, parsingModel),
297+
ActorType.FINAL to finalWriter(temperature, firstLevelModel, maxIterations = 10),
298+
)
299+
300+
private fun initialAuthor(temperature: Double, model: ChatModel, parsingModel: ChatModel) = ParsedActor(
301+
NodeList::class.java,
302+
prompt = """You are a helpful writing assistant. Respond in detail to the user's prompt""",
303+
model = model,
304+
temperature = temperature,
305+
parsingModel = parsingModel,
306+
describer = object : JsonDescriber(
307+
mutableSetOf("com.simiacryptus", "com.github.simiacryptus")
308+
) {
309+
override val includeMethods: Boolean get() = false
310+
},
311+
exampleInstance = exampleNodeList(),
312+
)
313+
314+
private fun exampleNodeList() = NodeList(
315+
listOf(
316+
OutlineManager.Node(name = "Main Idea", description = "Main Idea Description"),
317+
OutlineManager.Node(
318+
name = "Supporting Idea",
319+
description = "Supporting Idea Description",
320+
children = listOf(
321+
OutlineManager.Node(name = "Sub Idea", description = "Sub Idea Description")
322+
)
323+
)
324+
)
325+
)
326+
327+
private fun expansionAuthor(
328+
temperature: Double,
329+
parsingModel: ChatModel
330+
): ParsedActor<NodeList> =
331+
ParsedActor(
332+
resultClass = NodeList::class.java,
333+
prompt = """You are a helpful writing assistant. Provide additional details about the topic.""",
334+
name = "Expand",
335+
model = parsingModel,
336+
temperature = temperature,
337+
parsingModel = parsingModel,
338+
exampleInstance = exampleNodeList(),
339+
)
340+
341+
private fun finalWriter(temperature: Double, model: ChatModel, maxIterations: Int = 5) = LargeOutputActor(
342+
model = model,
343+
temperature = temperature,
344+
maxIterations = maxIterations,
345+
)
346+
347+
}
348+
}

0 commit comments

Comments
 (0)