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