diff --git a/LangGraph/Sources/LangGraph/AgentExecutor.swift b/LangGraph/Sources/LangGraph/AgentExecutor.swift index e921dfd..f8a6411 100644 --- a/LangGraph/Sources/LangGraph/AgentExecutor.swift +++ b/LangGraph/Sources/LangGraph/AgentExecutor.swift @@ -1,6 +1,6 @@ // // File.swift -// +// // // Created by bsorrentino on 12/03/24. // @@ -18,10 +18,11 @@ func loadPromptFromBundle( fileName: String ) throws -> String { guard let filepath = Bundle.module.path(forResource: fileName, ofType: "txt") else { throw _EX("prompt file \(fileName) not found!") } - + return try String(contentsOfFile: filepath, encoding: .utf8) } + struct DiagramParticipant : Codable { var name: String var shape: String @@ -38,19 +39,36 @@ struct DiagramContainer: Codable { var children: [String] // destination var description: String } + +enum DiagramNLPDescription : Codable { + case string(String) + case array([String]) + + init(from decoder: Decoder) throws { + let container = try decoder.singleValueContainer() + if let string = try? container.decode(String.self) { + self = .string(string) + } else if let array = try? container.decode([String].self) { + self = .array(array) + } else { + throw _EX("Expected string or array of strings") + } + } +} + struct DiagramDescription : Codable { var type: String var title: String var participants: [DiagramParticipant] var relations: [DiagramRelation] var containers: [DiagramContainer] - var description: [String] // NLP description + var description: DiagramNLPDescription // NLP description } struct AgentExecutorState : AgentState { var data: [String : Any] - + init() { data = [:] } @@ -59,18 +77,14 @@ struct AgentExecutorState : AgentState { data = initState } - var openAI:OpenAI? { - data["openai"] as? OpenAI - } - var diagramImageUrlOrData:String? { data["diagram_image_url_or_data"] as? String } - + var diagramCode:String? { data["diagram_code"] as? String } - + var diagram:DiagramDescription? { data["diagram"] as? DiagramDescription } @@ -78,14 +92,14 @@ struct AgentExecutorState : AgentState { func diagramDescriptionOutputParse( _ content: String ) throws -> DiagramDescription { - let regex = #/```(json)?(?.*[^`]{3})(```)?/#.dotMatchesNewlines() + let regex = #/```(json\n)?({)(?.*)(}\n(```)?)/#.dotMatchesNewlines() - if let match = try regex.wholeMatch(in: content) { + if let match = try regex.firstMatch(in: content) { let decoder = JSONDecoder() - let code = match.code + let code = "{\(match.code)}" if let data = code.data(using: .utf8) { @@ -98,19 +112,20 @@ func diagramDescriptionOutputParse( _ content: String ) throws -> DiagramDescrip else { throw _EX( "content doesn't match schema!") } - + } -func describeDiagramImage( state: AgentExecutorState ) async throws -> PartialAgentState { +func describeDiagramImage( state: AgentExecutorState, + openAI:OpenAI, + delegate:T ) async throws -> PartialAgentState { - guard let openAI = state.openAI else { - throw _EX("OpenAI not initialized!") - } guard let imageUrl = state.diagramImageUrlOrData else { throw _EX("diagramImageUrlOrData not initialized!") } + await delegate.progress("starting analyze\ndiagram 👀") + let prompt = try loadPromptFromBundle(fileName: "describe_diagram_prompt") let query = ChatQuery( @@ -123,33 +138,47 @@ func describeDiagramImage( state: AgentExecutorState ) async throws -> PartialAg ], maxTokens: 2000 ) - + let chatResult = try await openAI.chats(query: query) + await delegate.progress( "diagram processed ✅") + let result = chatResult.choices[0].message.content - + if case .string(let content) = result { - return [ "diagram": try diagramDescriptionOutputParse( content ) ] + let diagram = try diagramDescriptionOutputParse( content ) + + await delegate.progress( "diagram type\n '\(diagram.type)'") + + return [ "diagram": diagram ] } throw _EX("invalid content") } -func translateSequenceDiagramDescriptionToPlantUML( state: AgentExecutorState ) async throws -> PartialAgentState { +func translateSequenceDiagramDescriptionToPlantUML( state: AgentExecutorState, + openAI:OpenAI, + delegate:T ) async throws -> PartialAgentState { - guard let openAI = state.openAI else { - throw _EX("OpenAI not initialized!") - } guard let diagram = state.diagram else { throw _EX("diagram not initialized!") } + await delegate.progress("starting translate diagram into sequence Diagram") + var prompt = try loadPromptFromBundle(fileName: "sequence_diagram_prompt") + + let description:String = switch(diagram.description) { + case .string(let string): + string + case .array(let array ): + array.joined(separator: "\n") + } prompt = prompt .replacingOccurrences(of: "{diagram_title}", with: diagram.title) - .replacingOccurrences(of: "{diagram_description}", with: diagram.description.joined(separator: "\n")) - + .replacingOccurrences(of: "{diagram_description}", with: description) + let query = ChatQuery( model: .gpt3_5Turbo, messages: [ @@ -160,22 +189,61 @@ func translateSequenceDiagramDescriptionToPlantUML( state: AgentExecutorState ) maxTokens: 2000 ) - let chatResult = try await openAI.chats(query: query) - - let result = chatResult.choices[0].message.content - - if case .string(let content) = result { - return [ "diagram_code": content ] - - } - + let chatResult = try await openAI.chats(query: query) + + let result = chatResult.choices[0].message.content + + guard case .string(let content) = result else { throw _EX( "invalid result!" ) - + } + + return [ "diagram_code": content ] + } -func translateGenericDiagramDescriptionToPlantUML( state: AgentExecutorState ) async throws -> PartialAgentState { +func translateGenericDiagramDescriptionToPlantUML( state: AgentExecutorState, + openAI:OpenAI, + delegate:T ) async throws -> PartialAgentState { + + guard let diagram = state.diagram else { + throw _EX("diagram not initialized!") + } + + await delegate.progress("starting translate diagram into generic Diagram") + + var prompt = try loadPromptFromBundle(fileName: "generic_diagram_prompt") + + let encoder = JSONEncoder() + + let data = try encoder.encode(diagram) + + guard let content = String(data: data, encoding: .utf8) else { + throw _EX("diagram encoding error!") + } + + prompt = prompt + .replacingOccurrences(of: "{diagram_description}", with: content) + + let query = ChatQuery( + model: .gpt3_5Turbo, + messages: [ + Chat(role: .user, content: [ + ChatContent(text: prompt), + ]) + ], + maxTokens: 2000 + ) + + let chatResult = try await openAI.chats(query: query) + + let result = chatResult.choices[0].message.content + + guard case .string(let content) = result else { + throw _EX( "invalid result!" ) + } + + return [ "diagram_code": content ] - return [:] } func routeDiagramTranslation( state: AgentExecutorState ) async throws -> String { @@ -190,14 +258,25 @@ func routeDiagramTranslation( state: AgentExecutorState ) async throws -> String } } -public func agentExecutor( openAI: OpenAI, imageUrl: String ) async throws -> String? { +@MainActor /* @objc */ public protocol AgentExecutorDelegate { + + /* @objc optional */ func progress(_ message: String) -> Void +} + +public func agentExecutor( openAI: OpenAI, imageUrl: String, delegate:T ) async throws -> String? { let workflow = GraphState( stateType: AgentExecutorState.self ) - try workflow.addNode("agent_describer", action: describeDiagramImage ) - try workflow.addNode("agent_sequence_plantuml", action: translateSequenceDiagramDescriptionToPlantUML) - try workflow.addNode("agent_generic_plantuml", action: translateGenericDiagramDescriptionToPlantUML) - + try workflow.addNode("agent_describer", action: { state in + try await describeDiagramImage(state: state, openAI: openAI, delegate: delegate) + }) + try workflow.addNode("agent_sequence_plantuml", action: { state in + try await translateSequenceDiagramDescriptionToPlantUML( state: state, openAI:openAI, delegate:delegate ) + }) + try workflow.addNode("agent_generic_plantuml", action: { state in + try await translateGenericDiagramDescriptionToPlantUML( state: state, openAI:openAI, delegate:delegate ) + }) + try workflow.addEdge(sourceId: "agent_sequence_plantuml", targetId: END) try workflow.addEdge(sourceId: "agent_generic_plantuml", targetId: END) @@ -210,16 +289,16 @@ public func agentExecutor( openAI: OpenAI, imageUrl: String ) async throws -> St ] ) workflow.setEntryPoint( "agent_describer") - + let app = try workflow.compile() - + let inputs:[String : Any] = [ "openai": openAI, "diagram_image_url_or_data": imageUrl - ] - + ] + let response = try await app.invoke( inputs: inputs) - + return response.diagramCode } diff --git a/LangGraph/Tests/LangGraphTests/AgentExecutorTests.swift b/LangGraph/Tests/LangGraphTests/AgentExecutorTests.swift new file mode 100644 index 0000000..37342f4 --- /dev/null +++ b/LangGraph/Tests/LangGraphTests/AgentExecutorTests.swift @@ -0,0 +1,140 @@ +// +// AgentExecutorTests.swift +// +// +// Created by bsorrentino on 13/03/24. +// + +import XCTest +@testable import LangGraph + + + +final class AgentExecutorTests : XCTestCase { + + + func testLoadPrompt() async throws { + let describe = try loadPromptFromBundle( fileName: "describe_diagram_prompt" ) + + XCTAssertNotNil(describe) + + print( describe ) + + let generic = try loadPromptFromBundle( fileName: "generic_diagram_prompt" ) + + XCTAssertNotNil(generic) + + print( generic ) + + let sequence = try loadPromptFromBundle( fileName: "sequence_diagram_prompt" ) + + XCTAssertNotNil(sequence) + + print( sequence ) + + } + + func testParseDiagramDescriptionOutput() async throws { + + let output = +""" +```json +{ + "type": "process", + "title": "LLM Application Data Flow", + "participants": [ + { "name": "Event Stream", "shape": "cylinder", "description": "Source of event data" }, + { "name": "Preprocessing", "shape": "rectangle", "description": "Initial processing of events" }, + { "name": "LLM Application", "shape": "rectangle", "description": "Main application processing events" }, + { "name": "Postprocessing", "shape": "rectangle", "description": "Processing after main application" }, + { "name": "Output", "shape": "cylinder", "description": "Final output of the data flow" }, + { "name": "Observability", "shape": "rectangle", "description": "Metrics and logs monitoring" }, + { "name": "LLM Service", "shape": "rectangle", "description": "External service for LLM (e.g., OpenAI)" }, + { "name": "LLM Tracing", "shape": "rectangle", "description": "Tracing service for LLM (e.g., LangSmith)" } + ], + "relations": [ + { "source": "Event Stream", "target": "Preprocessing", "description": "Feeds into" }, + { "source": "Preprocessing", "target": "LLM Application", "description": "Feeds into" }, + { "source": "LLM Application", "target": "Postprocessing", "description": "Feeds into" }, + { "source": "Postprocessing", "target": "Output", "description": "Feeds into" }, + { "source": "LLM Application", "target": "Observability", "description": "Sends data to" }, + { "source": "LLM Application", "target": "LLM Service", "description": "Interacts with" }, + { "source": "LLM Application", "target": "LLM Tracing", "description": "Interacts with" } + ], + "containers": [ + { "name": "Stream Processor", "children": ["Preprocessing", "LLM Application", "Postprocessing"], "description": "Processes the event stream" } + ], + "description": [ + "The Event Stream is the starting point, which feeds into Preprocessing.", + "Preprocessing is part of the Stream Processor and prepares data for the LLM Application.", + "The LLM Application processes the data and may interact with external services like LLM Service and LLM Tracing.", + "After processing, the data is sent to Postprocessing, which is also part of the Stream Processor.", + "The Postprocessing stage prepares the final Output.", + "Throughout the process, the LLM Application sends data to Observability for monitoring purposes." + ] +} +``` +""" + + let diagram = try diagramDescriptionOutputParse( output ) + + XCTAssertNotNil(diagram) + XCTAssertEqual("process", diagram.type, "Type are different!") + XCTAssertEqual("LLM Application Data Flow", diagram.title, "Title are different!") + + let encoder = JSONEncoder() + + let data = try encoder.encode(diagram) + + let content = String(data: data, encoding: .utf8) + + XCTAssertNotNil(content, "data conversion error!") + + print( "===> OUTPUT ") + print( content! ) + } + + func testParseDiagramDescriptionOutputWithComment() async throws { + + let output = +""" +The image contains a hand-drawn flowchart with four distinct shapes connected by arrows. Here\'s the flowchart translated into the provided diagram-as-code syntax in JSON format:\n\n```json\n{\n \"type\": \"process\",\n \"title\": \"Flowchart for Selecting a Diagram Type\",\n \"participants\": [\n { \"name\": \"DescribeDiagram\", \"shape\": \"rectangle\", \"description\": \"Box labeled DESCRIBE DIAGRAM\" },\n { \"name\": \"ChooseType\", \"shape\": \"rectangle\", \"description\": \"Box labeled CHOOSE TYPE\" },\n { \"name\": \"Sequence\", \"shape\": \"rectangle\", \"description\": \"Box labeled SEQUENCE\" },\n { \"name\": \"Generic\", \"shape\": \"rectangle\", \"description\": \"Box labeled GENERIC\" },\n { \"name\": \"Stop\", \"shape\": \"ellipse\", \"description\": \"Ellipse labeled STOP\" }\n ],\n \"relations\": [\n { \"source\": \"DescribeDiagram\", \"target\": \"ChooseType\", \"description\": \"Arrow from DESCRIBE DIAGRAM to CHOOSE TYPE\" },\n { \"source\": \"ChooseType\", \"target\": \"Sequence\", \"description\": \"Arrow from CHOOSE TYPE to SEQUENCE\" },\n { \"source\": \"ChooseType\", \"target\": \"Generic\", \"description\": \"Arrow from CHOOSE TYPE to GENERIC\" },\n { \"source\": \"Sequence\", \"target\": \"Stop\", \"description\": \"Arrow from SEQUENCE to STOP\" },\n { \"source\": \"Generic\", \"target\": \"Stop\", \"description\": \"Arrow from GENERIC to STOP\" }\n ],\n \"containers\": [],\n \"description\": [\n \"Step 1: Start at the \'DESCRIBE DIAGRAM\' rectangle.\",\n \"Step 2: Move down to the \'CHOOSE TYPE\' rectangle.\",\n \"Step 3: From \'CHOOSE TYPE\', two options diverge, leading either left to \'SEQUENCE\' or right to \'GENERIC\'.\",\n \"Step 4: Both \'SEQUENCE\' and \'GENERIC\' point to the \'STOP\' ellipse.\"\n ]\n}\n```\n\nPlease, ensure to use the correct terms and formatting when you convert this JSON representation back to diagram-as-code syntax as per the requirements of the tool or language you are using. +""" + + let diagram = try diagramDescriptionOutputParse( output ) + + XCTAssertNotNil(diagram) + XCTAssertEqual("process", diagram.type, "Type are different!") + XCTAssertEqual("Flowchart for Selecting a Diagram Type", diagram.title, "Title are different!") + + let encoder = JSONEncoder() + + let data = try encoder.encode(diagram) + + let content = String(data: data, encoding: .utf8) + + XCTAssertNotNil(content, "data conversion error!") + + print( "===> OUTPUT ") + print( content! ) + } + + func testCodableDiagramDescriptionOutput() async throws { + + let diagram = +""" +{\n \"type\": \"flowchart\",\n \"title\": \"Diagram Translation Process\",\n \"participants\": [\n { \"name\": \"Describe Diagram\", \"shape\": \"rectangle\", \"description\": \"Initial step to describe the diagram\" },\n { \"name\": \"Check Type\", \"shape\": \"rectangle\", \"description\": \"Check the type of diagram\" },\n { \"name\": \"Sequence\", \"shape\": \"rectangle\", \"description\": \"Option for a sequence diagram\" },\n { \"name\": \"Generic\", \"shape\": \"rectangle\", \"description\": \"Option for a generic diagram\" },\n { \"name\": \"PlantUML Sequence\", \"shape\": \"rectangle\", \"description\": \"Command to generate a sequence diagram with PlantUML\" },\n { \"name\": \"PlantUML Generic\", \"shape\": \"rectangle\", \"description\": \"Command to generate a generic diagram with PlantUML\" },\n { \"name\": \"Stop\", \"shape\": \"ellipse\", \"description\": \"End of process\" }\n ],\n \"relations\": [\n { \"source\": \"Describe Diagram\", \"target\": \"Check Type\", \"description\": \"Leads to checking the diagram type\" },\n { \"source\": \"Check Type\", \"target\": \"Sequence\", \"description\": \"Option selected for sequence diagrams\" },\n { \"source\": \"Check Type\", \"target\": \"Generic\", \"description\": \"Option selected for generic diagrams\" },\n { \"source\": \"Sequence\", \"target\": \"PlantUML Sequence\", \"description\": \"Leads to PlantUML sequence diagram creation\" },\n { \"source\": \"Generic\", \"target\": \"PlantUML Generic\", \"description\": \"Leads to PlantUML generic diagram creation\" },\n { \"source\": \"PlantUML Sequence\", \"target\": \"Stop\", \"description\": \"Ends the sequence diagram process\" },\n { \"source\": \"PlantUML Generic\", \"target\": \"Stop\", \"description\": \"Ends the generic diagram process\" }\n ],\n \"containers\": [],\n \"description\": \"The flowchart initiates with \'Describe Diagram\', followed by \'Check Type\' to determine if the diagram is a \'Sequence\' or \'Generic\' diagram. According to the type, it either proceeds to \'PlantUML Sequence\' or \'PlantUML Generic\' for execution. The process concludes at the \'Stop\' step.\"\n}" +""" + + let encoder = JSONEncoder() + + let data = try encoder.encode(diagram) + + let content = String(data: data, encoding: .utf8) + + XCTAssertNotNil(content, "data conversion error!") + + print( "===> OUTPUT ") + print( content! ) + } +} diff --git a/LangGraph/Tests/LangGraphTests/LangGraphTests.swift b/LangGraph/Tests/LangGraphTests/LangGraphTests.swift index 86f4e81..6d3834c 100644 --- a/LangGraph/Tests/LangGraphTests/LangGraphTests.swift +++ b/LangGraph/Tests/LangGraphTests/LangGraphTests.swift @@ -254,75 +254,5 @@ final class LangGraphTests: XCTestCase { } - func testLoadPrompt() async throws { - let describe = try loadPromptFromBundle( fileName: "describe_diagram_prompt" ) - - XCTAssertNotNil(describe) - - print( describe ) - - let generic = try loadPromptFromBundle( fileName: "generic_diagram_prompt" ) - - XCTAssertNotNil(generic) - - print( generic ) - - let sequence = try loadPromptFromBundle( fileName: "sequence_diagram_prompt" ) - - XCTAssertNotNil(sequence) - - print( sequence ) - - } - - func testParseDiagramDescriptionOutput() async throws { - - let output = -""" -```json -{ - "type": "process", - "title": "LLM Application Data Flow", - "participants": [ - { "name": "Event Stream", "shape": "cylinder", "description": "Source of event data" }, - { "name": "Preprocessing", "shape": "rectangle", "description": "Initial processing of events" }, - { "name": "LLM Application", "shape": "rectangle", "description": "Main application processing events" }, - { "name": "Postprocessing", "shape": "rectangle", "description": "Processing after main application" }, - { "name": "Output", "shape": "cylinder", "description": "Final output of the data flow" }, - { "name": "Observability", "shape": "rectangle", "description": "Metrics and logs monitoring" }, - { "name": "LLM Service", "shape": "rectangle", "description": "External service for LLM (e.g., OpenAI)" }, - { "name": "LLM Tracing", "shape": "rectangle", "description": "Tracing service for LLM (e.g., LangSmith)" } - ], - "relations": [ - { "source": "Event Stream", "target": "Preprocessing", "description": "Feeds into" }, - { "source": "Preprocessing", "target": "LLM Application", "description": "Feeds into" }, - { "source": "LLM Application", "target": "Postprocessing", "description": "Feeds into" }, - { "source": "Postprocessing", "target": "Output", "description": "Feeds into" }, - { "source": "LLM Application", "target": "Observability", "description": "Sends data to" }, - { "source": "LLM Application", "target": "LLM Service", "description": "Interacts with" }, - { "source": "LLM Application", "target": "LLM Tracing", "description": "Interacts with" } - ], - "containers": [ - { "name": "Stream Processor", "children": ["Preprocessing", "LLM Application", "Postprocessing"], "description": "Processes the event stream" } - ], - "description": [ - "The Event Stream is the starting point, which feeds into Preprocessing.", - "Preprocessing is part of the Stream Processor and prepares data for the LLM Application.", - "The LLM Application processes the data and may interact with external services like LLM Service and LLM Tracing.", - "After processing, the data is sent to Postprocessing, which is also part of the Stream Processor.", - "The Postprocessing stage prepares the final Output.", - "Throughout the process, the LLM Application sends data to Observability for monitoring purposes." - ] -} -``` -""" - - let dd = try diagramDescriptionOutputParse( output ) - - XCTAssertNotNil(dd) - XCTAssertEqual("process", dd.type, "Type are different!") - XCTAssertEqual("LLM Application Data Flow", dd.title, "Title are different!") - - } } diff --git a/PlantUML/ActivityView.swift b/PlantUML/ActivityView.swift index bb3a249..e58b712 100644 --- a/PlantUML/ActivityView.swift +++ b/PlantUML/ActivityView.swift @@ -10,7 +10,7 @@ import SwiftUI struct ActivityView: View where Content: View { @Environment( \.colorScheme) var colorScheme - @Binding var isShowing: Bool + var isShowing: Bool var label: String = "Loading ..." var content: () -> Content @@ -35,7 +35,7 @@ struct ActivityView: View where Content: View { RoundedRectangle(cornerRadius: 10) .stroke( borderColor, lineWidth: 1) .frame( width: geometry.size.width * 0.4, - height: geometry.size.height * 0.1) + height: geometry.size.height * 0.2) .background() .overlay { ProgressView { @@ -59,7 +59,7 @@ struct ActivityView: View where Content: View { #Preview { - ActivityView(isShowing: .constant(true)) { + ActivityView(isShowing: true, label: "Loading\nprocess\nand\nother" ) { Text( "DRAWING" ) } diff --git a/PlantUML/OpenAIObservableService.swift b/PlantUML/OpenAIObservableService.swift index feba801..3f35f3d 100644 --- a/PlantUML/OpenAIObservableService.swift +++ b/PlantUML/OpenAIObservableService.swift @@ -149,10 +149,17 @@ class OpenAIObservableService : ObservableObject { } } + +} + +// LangGraph Exstension +extension OpenAIObservableService { + @MainActor - func vision( imageUrl: String ) async -> String? { + func processImageWithAgents( imageUrl: String, delegate:T ) async -> String? { - guard let openAI /*, let openAIModel */, case .Ready = status else { + guard let openAI, case .Ready = status else { + delegate.progress("WARNING: OpenAI API not initialized") return nil } @@ -160,7 +167,7 @@ class OpenAIObservableService : ObservableObject { do { - if let content = try await agentExecutor( openAI: openAI, imageUrl: imageUrl) { + if let content = try await agentExecutor( openAI: openAI, imageUrl: imageUrl, delegate:delegate) { status = .Ready @@ -170,10 +177,12 @@ class OpenAIObservableService : ObservableObject { .joined(separator: "\n" ) } + delegate.progress("ERROR: invalid result!") status = .Error( "invalid result!" ) } catch { + delegate.progress("ERROR: \(error.localizedDescription)") status = .Error( error.localizedDescription ) } @@ -181,16 +190,6 @@ class OpenAIObservableService : ObservableObject { } } - - -extension OpenAIObservableService { // LangGraph extension - - - - - -} - class LILOQueue { var elements:Array = [] diff --git a/PlantUML/PlantUMLDrawingView.swift b/PlantUML/PlantUMLDrawingView.swift index cb74362..48c30fb 100644 --- a/PlantUML/PlantUMLDrawingView.swift +++ b/PlantUML/PlantUMLDrawingView.swift @@ -8,6 +8,7 @@ import SwiftUI import PencilKit import OpenAI +import LangGraph #Preview( "PlantUMLDrawingView") { NavigationStack { @@ -28,10 +29,10 @@ struct PlantUMLDrawingView: View { @ObservedObject var document: PlantUMLObservableDocument @State var isUseDrawingTool = false @State var processing = false - + @State var processingLabel: String = "👀 Processing ..." var body: some View { - ActivityView(isShowing: $processing, label: "👀 Processing ..." ) { + ActivityView(isShowing: processing, label: processingLabel ) { DrawingView(canvas: $canvas, isUsePickerTool: $isUseDrawingTool, data: document.drawing ) .font(.system(size: 35)) @@ -84,8 +85,12 @@ struct PlantUMLDrawingView: View { /// //MARK: - Vision extension /// -extension PlantUMLDrawingView { +extension PlantUMLDrawingView : AgentExecutorDelegate { + func progress(_ message: String) { + processingLabel = message + } + func processImage() { // getting image from Canvas @@ -110,14 +115,20 @@ extension PlantUMLDrawingView { processing.toggle() isUseDrawingTool = false + service.status = .Ready Task { - if let content = await service.vision( imageUrl: "data:image/png;base64,\(base64Image)" ) { - - document.text = content - document.drawing = canvas.drawing.dataRepresentation() - dismiss() - + do { + defer { + document.drawing = canvas.drawing.dataRepresentation() + dismiss() + } + + if let content = await service.processImageWithAgents( imageUrl: "data:image/png;base64,\(base64Image)", delegate: self ) { + + document.text = content + + } } } diff --git a/PlantUML/PlantUMLOpenAIView.swift b/PlantUML/PlantUMLOpenAIView.swift index adfac7a..d018efd 100644 --- a/PlantUML/PlantUMLOpenAIView.swift +++ b/PlantUML/PlantUMLOpenAIView.swift @@ -251,8 +251,6 @@ extension OpenAIView { HStack { Text("OpenAI Secrets") HideToggleButton(hidden: $hideOpenAISecrets) -// Divider() -// Button( action: { p.scrollTo("openai-settings", anchor: .top) }, label: { Text("More .....").font(.footnote) } ) } .id( "openai-secret") @@ -264,27 +262,7 @@ extension OpenAIView { Spacer() } } - -// Section { -// Picker("Model", selection: $service.inputModel) { -// ForEach(service.models, id: \.self) { -// Text($0) -// } -// } -// } -// header: { -// HStack { -// Text("OpenAI Extra settings") -// Divider() -// Button( action: { p.scrollTo("openai-secret", anchor: .bottom) }, label: { Text("Back ...").font(.footnote) } ) -// } -// .id( "openai-settings") -// } -// footer: { -// Rectangle().fill(Color.clear).frame(height: 65) -// -// } - + } } HStack { diff --git a/PlantUML/SwiftUI+PencilKit.swift b/PlantUML/SwiftUI+PencilKit.swift index 789788e..95b912b 100644 --- a/PlantUML/SwiftUI+PencilKit.swift +++ b/PlantUML/SwiftUI+PencilKit.swift @@ -32,8 +32,6 @@ struct DrawingView: UIViewRepresentable { @Binding var isUsePickerTool: Bool var data: Data? - var type: PKInkingTool.InkType = .pencil - var color: Color = .black @State var picker = PKToolPicker() // let eraser = PKEraserTool(.bitmap) @@ -49,7 +47,7 @@ struct DrawingView: UIViewRepresentable { } canvas.drawingPolicy = .anyInput - canvas.tool = PKInkingTool(type, color: UIColor(color)) + canvas.tool = PKInkingTool(.pen, color: UIColor(.black)) canvas.becomeFirstResponder()