From ce55f3459e7b4edb5bd6671beaf1f8b045cbf041 Mon Sep 17 00:00:00 2001 From: alepmedeiros Date: Mon, 31 Aug 2026 07:21:26 -0300 Subject: [PATCH 1/4] Add Dext.AI.Graph: LangGraph-style agent orchestration for Dext.AI.Agent Adds a graph-based orchestration layer (TAgentGraph/ICompiledAgent) on top of the existing Dext.AI.Agent ReAct runner, with immutable state, fixed and conditional edges, checkpointing (memory/file), and human-in-the-loop approval via RequireApproval/InterruptBefore + Resume/Cancel. Includes the GraphDemo console sample (and the untracked AgentDemo sample). Verified via live run: tool calls, HITL pause/resume, and cross-turn history through the checkpointer all behave as specified. Co-Authored-By: Claude Sonnet 5 --- Examples/AI/AgentDemo/AgentDemo.dpr | 228 ++++++++++ Examples/AI/AgentDemo/AgentDemo.dproj | 142 ++++++ Examples/AI/AgentDemo/AgentDemo.res | Bin 0 -> 152412 bytes Examples/AI/AgentDemo/appsettings.json | 12 + Examples/AI/GraphDemo/GraphDemo.dpr | 264 +++++++++++ Examples/AI/GraphDemo/GraphDemo.dproj | 140 ++++++ Examples/AI/GraphDemo/GraphDemo.res | Bin 0 -> 152412 bytes Sources/AI/Agent/Dext.AI.Agent.Contracts.pas | 171 +++++++ Sources/AI/Agent/Dext.AI.Agent.Factory.pas | 98 ++++ Sources/AI/Agent/Dext.AI.Agent.Observer.pas | 78 ++++ Sources/AI/Agent/Dext.AI.Agent.Runner.pas | 321 +++++++++++++ .../Dext.AI.Agent.Provider.Anthropic.pas | 323 +++++++++++++ .../Dext.AI.Agent.Provider.Ollama.pas | 269 +++++++++++ .../Dext.AI.Agent.Provider.OpenAI.pas | 291 ++++++++++++ .../AI/Graph/Dext.AI.Graph.Checkpointer.pas | 152 +++++++ Sources/AI/Graph/Dext.AI.Graph.Compiled.pas | 359 +++++++++++++++ Sources/AI/Graph/Dext.AI.Graph.Contracts.pas | 73 +++ Sources/AI/Graph/Dext.AI.Graph.Edge.pas | 91 ++++ Sources/AI/Graph/Dext.AI.Graph.Graph.pas | 331 ++++++++++++++ Sources/AI/Graph/Dext.AI.Graph.State.pas | 427 ++++++++++++++++++ .../AI/Graph/Nodes/Dext.AI.Graph.Node.LLM.pas | 123 +++++ .../Graph/Nodes/Dext.AI.Graph.Node.Tools.pas | 278 ++++++++++++ 22 files changed, 4171 insertions(+) create mode 100644 Examples/AI/AgentDemo/AgentDemo.dpr create mode 100644 Examples/AI/AgentDemo/AgentDemo.dproj create mode 100644 Examples/AI/AgentDemo/AgentDemo.res create mode 100644 Examples/AI/AgentDemo/appsettings.json create mode 100644 Examples/AI/GraphDemo/GraphDemo.dpr create mode 100644 Examples/AI/GraphDemo/GraphDemo.dproj create mode 100644 Examples/AI/GraphDemo/GraphDemo.res create mode 100644 Sources/AI/Agent/Dext.AI.Agent.Contracts.pas create mode 100644 Sources/AI/Agent/Dext.AI.Agent.Factory.pas create mode 100644 Sources/AI/Agent/Dext.AI.Agent.Observer.pas create mode 100644 Sources/AI/Agent/Dext.AI.Agent.Runner.pas create mode 100644 Sources/AI/Agent/Providers/Dext.AI.Agent.Provider.Anthropic.pas create mode 100644 Sources/AI/Agent/Providers/Dext.AI.Agent.Provider.Ollama.pas create mode 100644 Sources/AI/Agent/Providers/Dext.AI.Agent.Provider.OpenAI.pas create mode 100644 Sources/AI/Graph/Dext.AI.Graph.Checkpointer.pas create mode 100644 Sources/AI/Graph/Dext.AI.Graph.Compiled.pas create mode 100644 Sources/AI/Graph/Dext.AI.Graph.Contracts.pas create mode 100644 Sources/AI/Graph/Dext.AI.Graph.Edge.pas create mode 100644 Sources/AI/Graph/Dext.AI.Graph.Graph.pas create mode 100644 Sources/AI/Graph/Dext.AI.Graph.State.pas create mode 100644 Sources/AI/Graph/Nodes/Dext.AI.Graph.Node.LLM.pas create mode 100644 Sources/AI/Graph/Nodes/Dext.AI.Graph.Node.Tools.pas diff --git a/Examples/AI/AgentDemo/AgentDemo.dpr b/Examples/AI/AgentDemo/AgentDemo.dpr new file mode 100644 index 00000000..a8657494 --- /dev/null +++ b/Examples/AI/AgentDemo/AgentDemo.dpr @@ -0,0 +1,228 @@ +program AgentDemo; + +{$APPTYPE CONSOLE} + +uses + Winapi.Windows, + System.SysUtils, + System.Math, + System.JSON, + System.IOUtils, + System.Classes, + Dext.AI.MCP.Tools, + Dext.AI.MCP.Types, + Dext.AI.MCP.Protocol, + Dext.AI.MCP.Attributes, + Dext.AI.Agent.Contracts, + Dext.AI.Agent.Factory, + Dext.AI.Agent.Runner, + Dext.AI.Agent.Observer; + +// ─── Tools de demonstração ──────────────────────────────────────────────── +// Reutiliza EXATAMENTE o padrão TMCPToolProvider do Dext existente + +type + TFileSystemTools = class(TMCPToolProvider) + public + [MCPTool('list_files', 'Lista arquivos em um diretório')] + [MCPParam('path', 'Caminho do diretório', ptString, True)] + [MCPParam('extension', 'Filtro de extensão ex: .pas (opcional)', ptString, False)] + function ListFiles(const Args: TJSONObject): TMCPToolResult; + + [MCPTool('read_file', 'Lê o conteúdo de um arquivo texto')] + [MCPParam('path', 'Caminho completo do arquivo', ptString, True)] + [MCPParam('max_lines', 'Máximo de linhas (default: 50)', ptInteger, False)] + function ReadFile(const Args: TJSONObject): TMCPToolResult; + + [MCPTool('count_lines', 'Conta linhas de código em um arquivo')] + [MCPParam('path', 'Caminho completo do arquivo', ptString, True)] + function CountLines(const Args: TJSONObject): TMCPToolResult; + end; + +function TFileSystemTools.ListFiles(const Args: TJSONObject): TMCPToolResult; +var + Path, Ext, Pattern: string; + SR: TSearchRec; + JA: TJSONArray; +begin + Path := Args.GetValue('path', '.'); + Ext := Args.GetValue('extension', ''); + + if not DirectoryExists(Path) then + Exit(TMCPToolResult.Error('Diretório não encontrado: ' + Path)); + + Pattern := IncludeTrailingPathDelimiter(Path) + '*' + Ext; + JA := TJSONArray.Create; + try + if FindFirst(Pattern, faAnyFile, SR) = 0 then + try + repeat + if (SR.Attr and faDirectory) = 0 then + begin + var JO := TJSONObject.Create; + JO.AddPair('name', SR.Name); + JO.AddPair('size', TJSONNumber.Create(SR.Size)); + JA.AddElement(JO); + end; + until FindNext(SR) <> 0; + finally + FindClose(SR); + end; + Result := TMCPToolResult.Text( + Format('{"path":"%s","filter":"%s","count":%d,"files":%s}', + [Path, Ext, JA.Count, JA.ToJSON]) + ); + finally + JA.Free; + end; +end; + +function TFileSystemTools.ReadFile(const Args: TJSONObject): TMCPToolResult; +var + Path: string; + MaxLines, I: Integer; + Lines: TStringList; + SB: TStringBuilder; +begin + Path := Args.GetValue('path', ''); + MaxLines := Args.GetValue('max_lines', 50); + + if not FileExists(Path) then + Exit(TMCPToolResult.Error('Arquivo não encontrado: ' + Path)); + + Lines := TStringList.Create; + SB := TStringBuilder.Create; + try + Lines.LoadFromFile(Path, TEncoding.UTF8); + SB.AppendLine(Format('// %s — %d linhas total', [ExtractFileName(Path), Lines.Count])); + for I := 0 to Min(MaxLines - 1, Lines.Count - 1) do + SB.AppendFormat('%4d %s', [I + 1, Lines[I]]).AppendLine; + if Lines.Count > MaxLines then + SB.AppendLine(Format('// ... (%d linhas restantes)', [Lines.Count - MaxLines])); + Result := TMCPToolResult.Text(SB.ToString); + finally + Lines.Free; + SB.Free; + end; +end; + +function TFileSystemTools.CountLines(const Args: TJSONObject): TMCPToolResult; +var + Path: string; + Lines: TStringList; + Code, Comment, Blank: Integer; + Line: string; +begin + Path := Args.GetValue('path', ''); + if not FileExists(Path) then + Exit(TMCPToolResult.Error('Arquivo não encontrado: ' + Path)); + + Lines := TStringList.Create; + try + Lines.LoadFromFile(Path, TEncoding.UTF8); + Code := 0; Comment := 0; Blank := 0; + for Line in Lines do + begin + var T := Line.Trim; + if T = '' then Inc(Blank) + else if T.StartsWith('//') or T.StartsWith('{') or T.StartsWith('(*') then Inc(Comment) + else Inc(Code); + end; + Result := TMCPToolResult.Text( + Format('{"file":"%s","total":%d,"code":%d,"comments":%d,"blank":%d}', + [ExtractFileName(Path), Lines.Count, Code, Comment, Blank]) + ); + finally + Lines.Free; + end; +end; + +// ─── MAIN ───────────────────────────────────────────────────────────────── + +var + Config: TAgentConfig; + Provider: ILLMProvider; + Observer: IAgentObserver; + Runner: TAgentRunner; + Input: string; + Res: TAgentResult; + +begin + ReportMemoryLeaksOnShutdown := True; + + SetConsoleOutputCP(CP_UTF8); + SetConsoleCP(CP_UTF8); + + // ── Configuração via variável de ambiente ────────────────────────────── + // Para usar OpenAI: set OPENAI_API_KEY=sk-... + // Para usar Anthropic: set ANTHROPIC_API_KEY=sk-ant-... + // Para usar Ollama: (sem key — roda local) + + Config := TAgentConfig.OpenAI('gpt-4o'); // trocar aqui para mudar provider + Config.ApiKey := GetEnvironmentVariable('OPENAI_API_KEY'); + + // Descomente para usar Anthropic: + // Config := TAgentConfig.Anthropic('claude-sonnet-4-6'); + // Config.ApiKey := GetEnvironmentVariable('ANTHROPIC_API_KEY'); + + // Descomente para usar Ollama local (sem key): + // Config := TAgentConfig.Ollama('llama3.2'); + + Config.SystemPrompt := + 'Você é um assistente técnico especializado em projetos Delphi. ' + + 'Analise os arquivos do projeto e responda com precisão. ' + + 'Nunca invente dados — use apenas as tools disponíveis.'; + + if (Config.ApiKey = '') and not Config.ProviderString.StartsWith('ollama') then + begin + Writeln('ERRO: API key não encontrada.'); + Writeln('Para OpenAI: set OPENAI_API_KEY=sk-...'); + Writeln('Para Anthropic: set ANTHROPIC_API_KEY=sk-ant-...'); + ExitCode := 1; + Exit; + end; + + // ── Criar provider via Factory (igual ao init_chat_model do LangChain) ─ + try + Provider := TLLMFactory.CreateProvider(Config); + except + on E: ELLMProviderError do + begin + Writeln('ERRO: ' + E.Message); + ExitCode := 1; + Exit; + end; + end; + + Observer := TConsoleObserver.Create; + Runner := TAgentRunner.Create(Provider, Config, Observer); + try + Runner.RegisterProvider(TFileSystemTools.Create); + + Writeln('═══════════════════════════════════════════════════════'); + Writeln(Format(' Dext.AI.Agent — Demo ao Vivo', [])); + Writeln(Format(' Provider: %s | Model: %s', [Provider.ProviderName, Provider.ModelName])); + Writeln(' Digite sua pergunta. Enter em branco para sair.'); + Writeln('═══════════════════════════════════════════════════════'); + Writeln; + + repeat + Write('Pergunta: '); + Readln(Input); + if Input.Trim = '' then Break; + + Writeln; + Res := Runner.Run(Input); + + if not Res.Success then + begin + Writeln; + Writeln('ERRO: ' + Res.ErrorMsg); + end; + Writeln; + until False; + + finally + Runner.Free; + end; +end. diff --git a/Examples/AI/AgentDemo/AgentDemo.dproj b/Examples/AI/AgentDemo/AgentDemo.dproj new file mode 100644 index 00000000..10cb18f9 --- /dev/null +++ b/Examples/AI/AgentDemo/AgentDemo.dproj @@ -0,0 +1,142 @@ + + + True + Console + Debug + None + AgentDemo.dpr + Win32 + {B168E87A-A25E-49A2-806C-D4F584E6FEAD} + AgentDemo + 20.4 + 3 + + + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_2 + true + true + + + true + Cfg_2 + true + true + + + AgentDemo + ..\..\Output\$(ProductVersion)\$(Platform)\$(Config) + ..\..\Output\ + 00400000 + System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace) + ..\..\..\Sources\AI\Agent;..\..\..\Sources\AI\Agent\Providers;..\..\..\Output\$(ProductVersion)\$(Platform)\$(Config);$(DCC_UnitSearchPath) + $(BDS)\bin\delphi_PROJECTICNS.icns + $(BDS)\bin\delphi_PROJECTICON.ico + + CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=;CFBundleName= + 1046 + + + Debug + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + + + Debug + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace) + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + + + 0 + RELEASE;$(DCC_Define) + false + 0 + + + DEBUG;$(DCC_Define) + true + true + false + true + + + none + true + true + true + + + none + + + + MainSource + + + Base + + + Cfg_1 + Base + + + Cfg_2 + Base + + + + Delphi.Personality.12 + + + + + AgentDemo.dpr + + + + + False + False + False + False + False + True + True + False + False + False + False + + + 12 + + + + + diff --git a/Examples/AI/AgentDemo/AgentDemo.res b/Examples/AI/AgentDemo/AgentDemo.res new file mode 100644 index 0000000000000000000000000000000000000000..e30ea808a0625e531bc6e2d44971efe04e66a16e GIT binary patch literal 152412 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 literal 0 HcmV?d00001 diff --git a/Examples/AI/AgentDemo/appsettings.json b/Examples/AI/AgentDemo/appsettings.json new file mode 100644 index 00000000..23d3072e --- /dev/null +++ b/Examples/AI/AgentDemo/appsettings.json @@ -0,0 +1,12 @@ +{ + "Provider": "openai:gpt-4o", + "OpenAI": { + "ApiKeyEnv": "OPENAI_API_KEY" + }, + "Anthropic": { + "ApiKeyEnv": "ANTHROPIC_API_KEY" + }, + "Ollama": { + "BaseUrl": "http://localhost:11434" + } +} diff --git a/Examples/AI/GraphDemo/GraphDemo.dpr b/Examples/AI/GraphDemo/GraphDemo.dpr new file mode 100644 index 00000000..8b239b5e --- /dev/null +++ b/Examples/AI/GraphDemo/GraphDemo.dpr @@ -0,0 +1,264 @@ +program GraphDemo; + +{$APPTYPE CONSOLE} + +uses + Winapi.Windows, + System.SysUtils, + System.Math, + System.JSON, + System.IOUtils, + System.Classes, + Dext.AI.MCP.Tools, + Dext.AI.MCP.Types, + Dext.AI.MCP.Protocol, + Dext.AI.MCP.Attributes, + Dext.AI.Agent.Contracts, + Dext.AI.Agent.Factory, + Dext.AI.Agent.Observer, + Dext.AI.Graph.Contracts, + Dext.AI.Graph.State, + Dext.AI.Graph.Edge, + Dext.AI.Graph.Graph, + Dext.AI.Graph.Compiled, + Dext.AI.Graph.Checkpointer, + Dext.AI.Graph.Node.LLM, + Dext.AI.Graph.Node.Tools; + +type + TFileSystemTools = class(TMCPToolProvider) + public + [MCPTool('list_files', 'Lista arquivos em um diretório')] + [MCPParam('path', 'Caminho do diretório', ptString, True)] + [MCPParam('extension', 'Filtro ex: .pas (opcional)', ptString, False)] + function ListFiles(const Args: TJSONObject): TMCPToolResult; + + [MCPTool('read_file', 'Lê o conteúdo de um arquivo')] + [MCPParam('path', 'Caminho completo do arquivo', ptString, True)] + [MCPParam('max_lines', 'Máximo de linhas (default: 50)', ptInteger, False)] + function ReadFile(const Args: TJSONObject): TMCPToolResult; + + [MCPTool('count_lines', 'Conta linhas de código em um arquivo')] + [MCPParam('path', 'Caminho completo do arquivo', ptString, True)] + function CountLines(const Args: TJSONObject): TMCPToolResult; + end; + +function TFileSystemTools.ListFiles(const Args: TJSONObject): TMCPToolResult; +var + Path, Ext, Pattern: string; + SR: TSearchRec; + JA: TJSONArray; +begin + Path := Args.GetValue('path', '.'); + Ext := Args.GetValue('extension', ''); + + if not DirectoryExists(Path) then + Exit(TMCPToolResult.Error('Diretório não encontrado: ' + Path)); + + Pattern := IncludeTrailingPathDelimiter(Path) + '*' + Ext; + JA := TJSONArray.Create; + try + if FindFirst(Pattern, faAnyFile, SR) = 0 then + try + repeat + if (SR.Attr and faDirectory) = 0 then + begin + var JO := TJSONObject.Create; + JO.AddPair('name', SR.Name); + JO.AddPair('size', TJSONNumber.Create(SR.Size)); + JA.AddElement(JO); + end; + until FindNext(SR) <> 0; + finally + FindClose(SR); + end; + Result := TMCPToolResult.Text( + Format('{"path":"%s","filter":"%s","count":%d,"files":%s}', + [Path, Ext, JA.Count, JA.ToJSON]) + ); + finally + JA.Free; + end; +end; + +function TFileSystemTools.ReadFile(const Args: TJSONObject): TMCPToolResult; +var + Path: string; + MaxLines, I: Integer; + Lines: TStringList; + SB: TStringBuilder; +begin + Path := Args.GetValue('path', ''); + MaxLines := Args.GetValue('max_lines', 50); + + if not FileExists(Path) then + Exit(TMCPToolResult.Error('Arquivo não encontrado: ' + Path)); + + Lines := TStringList.Create; + SB := TStringBuilder.Create; + try + Lines.LoadFromFile(Path, TEncoding.UTF8); + SB.AppendLine(Format('// %s — %d linhas total', [ExtractFileName(Path), Lines.Count])); + for I := 0 to Min(MaxLines - 1, Lines.Count - 1) do + SB.AppendFormat('%4d %s', [I + 1, Lines[I]]).AppendLine; + if Lines.Count > MaxLines then + SB.AppendLine(Format('// ... (%d linhas restantes)', [Lines.Count - MaxLines])); + Result := TMCPToolResult.Text(SB.ToString); + finally + Lines.Free; + SB.Free; + end; +end; + +function TFileSystemTools.CountLines(const Args: TJSONObject): TMCPToolResult; +var + Path: string; + Lines: TStringList; + Code, Comment, Blank: Integer; + Line: string; +begin + Path := Args.GetValue('path', ''); + if not FileExists(Path) then + Exit(TMCPToolResult.Error('Arquivo não encontrado: ' + Path)); + + Lines := TStringList.Create; + try + Lines.LoadFromFile(Path, TEncoding.UTF8); + Code := 0; Comment := 0; Blank := 0; + for Line in Lines do + begin + var T := Line.Trim; + if T = '' then Inc(Blank) + else if T.StartsWith('//') or T.StartsWith('{') or T.StartsWith('(*') then Inc(Comment) + else Inc(Code); + end; + Result := TMCPToolResult.Text( + Format('{"file":"%s","total":%d,"code":%d,"comments":%d,"blank":%d}', + [ExtractFileName(Path), Lines.Count, Code, Comment, Blank]) + ); + finally + Lines.Free; + end; +end; + +var + Config: TAgentConfig; + Provider: ILLMProvider; + Observer: IAgentObserver; + ToolsNode: TToolsNode; + LLMNode: TLLMNode; + Graph: TAgentGraph; + Agent: ICompiledAgent; + Checkpointer: ICheckpointer; + Input, ThreadId: string; + RunResult: TGraphRunResult; + Confirm: string; + +begin + ReportMemoryLeaksOnShutdown := True; + + SetConsoleOutputCP(CP_UTF8); + SetConsoleCP(CP_UTF8); + + Config := TAgentConfig.OpenAI('gpt-4o'); + Config.ApiKey := GetEnvironmentVariable('OPENAI_API_KEY'); + Config.SystemPrompt := + 'Você é um assistente técnico especializado em projetos Delphi. ' + + 'Use as tools disponíveis para responder. Nunca invente dados.'; + + if Config.ApiKey = '' then + begin + Writeln('ERRO: Defina OPENAI_API_KEY'); + ExitCode := 1; + Exit; + end; + + try + Provider := TLLMFactory.CreateProvider(Config); + except + on E: ELLMProviderError do + begin + Writeln('ERRO: ' + E.Message); + ExitCode := 1; + Exit; + end; + end; + + Observer := TConsoleObserver.Create; + ToolsNode := TToolsNode.Create; + LLMNode := nil; + try + ToolsNode.RegisterProvider(TFileSystemTools.Create); + LLMNode := TLLMNode.Create(ToolsNode.GetToolSchemas); + + Checkpointer := TMemoryCheckpointer.Create; + + Graph := TAgentGraph.Create; + try + Agent := Graph + .AddNode('call_llm', LLMNode.AsHandler) + .AddNode('execute_tools', ToolsNode.AsHandler) + .SetEntryPoint('call_llm') + .AddConditionalEdge('call_llm', + DefaultShouldContinue, + [TEdgeRoute.To_('execute_tools'), TEdgeRoute.ToEnd]) + .AddEdge('execute_tools', 'call_llm') + .RequireApproval('execute_tools') + .Compile(Provider, Config, Observer, Checkpointer); + finally + Graph.Free; + end; + + Writeln('═══════════════════════════════════════════════════════'); + Writeln(' Dext.AI.Graph — Demo (estilo LangGraph)'); + Writeln(Format(' Provider: %s | Model: %s', + [Provider.ProviderName, Provider.ModelName])); + Writeln(' Digite sua pergunta. Enter em branco para sair.'); + Writeln('═══════════════════════════════════════════════════════'); + Writeln; + + ThreadId := 'demo-session-001'; + + repeat + Write('Pergunta: '); + Readln(Input); + if Input.Trim = '' then + Break; + + Writeln; + RunResult := Agent.Run(Input, ThreadId); + + case RunResult.Status of + grsFinished: + Writeln(''); + + grsWaitingApproval: + begin + Writeln; + Writeln('⏸ Aguardando aprovação humana...'); + Writeln(' Nó pendente: ' + RunResult.PendingNode); + Write(' Aprovar? (s/n): '); + Readln(Confirm); + if Confirm.ToLower = 's' then + begin + RunResult := Agent.Resume(ThreadId); + Writeln(' Continuando...'); + end + else + begin + Agent.Cancel(ThreadId); + Writeln(' Cancelado.'); + end; + end; + + grsError: + Writeln('ERRO: ' + RunResult.ErrorMsg); + end; + Writeln; + until False; + finally + Agent := nil; + LLMNode.Free; + ToolsNode.Free; + end; +end. diff --git a/Examples/AI/GraphDemo/GraphDemo.dproj b/Examples/AI/GraphDemo/GraphDemo.dproj new file mode 100644 index 00000000..ccbfe80e --- /dev/null +++ b/Examples/AI/GraphDemo/GraphDemo.dproj @@ -0,0 +1,140 @@ + + + True + Console + Debug + None + GraphDemo.dpr + Win32 + {A7C3E91F-4B2D-48E1-9C6A-1F8D3E5B7024} + GraphDemo + 20.4 + 3 + + + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Base + true + + + true + Cfg_2 + true + true + + + true + Cfg_2 + true + true + + + GraphDemo + ..\..\Output\$(ProductVersion)\$(Platform)\$(Config) + ..\..\Output\ + 00400000 + System;Xml;Data;Datasnap;Web;Soap;$(DCC_Namespace) + ..\..\..\Sources\AI\Agent;..\..\..\Sources\AI\Agent\Providers;..\..\..\Sources\AI\MCP;..\..\..\Sources\AI\Graph;..\..\..\Sources\AI\Graph\Nodes;..\..\..\Sources\Core;..\..\..\Sources\Core\Base;..\..\..\Sources\Core\Json;..\..\..\Sources\Common;..\..\..\Output\$(ProductVersion)\$(Platform)\$(Config);..\..\..\Output\$(ProductVersion)\$(Platform)\Release + $(BDS)\bin\delphi_PROJECTICNS.icns + $(BDS)\bin\delphi_PROJECTICON.ico + CompanyName=;FileDescription=;FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProductName=;ProductVersion=1.0.0.0;Comments=;CFBundleName= + 1046 + + + Debug + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;Bde;$(DCC_Namespace) + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + + + Debug + Winapi;System.Win;Data.Win;Datasnap.Win;Web.Win;Soap.Win;Xml.Win;$(DCC_Namespace) + CompanyName=;FileDescription=$(MSBuildProjectName);FileVersion=1.0.0.0;InternalName=;LegalCopyright=;LegalTrademarks=;OriginalFilename=;ProgramID=com.embarcadero.$(MSBuildProjectName);ProductName=$(MSBuildProjectName);ProductVersion=1.0.0.0;Comments= + 1033 + + + 0 + RELEASE;$(DCC_Define) + false + 0 + + + DEBUG;$(DCC_Define) + true + true + false + true + + + none + true + true + true + + + none + + + + MainSource + + + Base + + + Cfg_1 + Base + + + Cfg_2 + Base + + + + Delphi.Personality.12 + + + + + GraphDemo.dpr + + + + + False + False + False + False + False + True + True + False + False + False + False + + + 12 + + + + + diff --git a/Examples/AI/GraphDemo/GraphDemo.res b/Examples/AI/GraphDemo/GraphDemo.res new file mode 100644 index 0000000000000000000000000000000000000000..e30ea808a0625e531bc6e2d44971efe04e66a16e GIT binary patch literal 152412 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 literal 0 HcmV?d00001 diff --git a/Sources/AI/Agent/Dext.AI.Agent.Contracts.pas b/Sources/AI/Agent/Dext.AI.Agent.Contracts.pas new file mode 100644 index 00000000..03cc4c80 --- /dev/null +++ b/Sources/AI/Agent/Dext.AI.Agent.Contracts.pas @@ -0,0 +1,171 @@ +{***************************************************************************} +{ } +{ Dext Framework } +{ } +{ Dext.AI.Agent - Multi-Provider LLM Agent } +{ } +{***************************************************************************} +{ } +{ Description: } +{ Core contracts for the Dext.AI.Agent framework: message/response } +{ types, the ILLMProvider strategy interface, agent configuration and } +{ the observer interface used to report ReAct loop progress. } +{ } +{***************************************************************************} +unit Dext.AI.Agent.Contracts; + +interface + +uses + System.SysUtils, System.JSON; + +type + TLLMRole = (lrSystem, lrUser, lrAssistant, lrToolResult); + TLLMStopReason = (srEndTurn, srToolUse, srMaxTokens, srError); + + TLLMToolCall = record + Id: string; + Name: string; + ArgsJson: string; + end; + + TLLMMessage = record + Role: TLLMRole; + Content: string; + ToolCallId: string; + ToolCalls: TArray; + public + class function User(const AContent: string): TLLMMessage; static; + class function System(const AContent: string): TLLMMessage; static; + class function Assistant(const AContent: string; + const AToolCalls: TArray = nil): TLLMMessage; static; + class function ToolResult(const AToolCallId, AContent: string): TLLMMessage; static; + end; + + TLLMResponse = record + Content: string; + StopReason: TLLMStopReason; + ToolCalls: TArray; + InputTokens: Integer; + OutputTokens: Integer; + end; + + TToolSchema = record + Name: string; + Description: string; + InputSchema: string; // JSON Schema serializado + end; + + // Interface única — o Strategy + ILLMProvider = interface + ['{A1B2C3D4-E5F6-7890-ABCD-EF1234567890}'] + function Complete( + const AMessages: TArray; + const ATools: TArray + ): TLLMResponse; + function ProviderName: string; + function ModelName: string; + end; + + TAgentConfig = record + ProviderString: string; // ex: 'openai:gpt-4o' ou 'anthropic:claude-sonnet-4-6' + ApiKey: string; + BaseUrl: string; // para Ollama: 'http://localhost:11434' + MaxTokens: Integer; + MaxIterations: Integer; + SystemPrompt: string; + public + class function OpenAI(const AModel: string = 'gpt-4o'): TAgentConfig; static; + class function Anthropic(const AModel: string = 'claude-sonnet-4-6'): TAgentConfig; static; + class function Ollama(const AModel: string = 'llama3.2'): TAgentConfig; static; + end; + + TAgentResult = record + FinalAnswer: string; + Iterations: Integer; + Success: Boolean; + ErrorMsg: string; + end; + + IAgentObserver = interface + ['{B2C3D4E5-F6A7-8901-BCDE-F12345678901}'] + procedure OnIterationStart(AIteration: Integer); + procedure OnToolCall(const AToolName, AArgsJson: string); + procedure OnToolResult(const AToolName, AResult: string); + procedure OnLLMResponse(const AContent: string; AStopReason: TLLMStopReason); + procedure OnFinished(const AAnswer: string; AIterations: Integer); + end; + + ELLMProviderError = class(Exception); + +implementation + +const + DEFAULT_SYSTEM_PROMPT = + 'You are a helpful assistant. Use the available tools to answer accurately. ' + + 'Never invent data — only use what the tools return.'; + +{ TLLMMessage } + +class function TLLMMessage.User(const AContent: string): TLLMMessage; +begin + Result := Default(TLLMMessage); + Result.Role := lrUser; + Result.Content := AContent; +end; + +class function TLLMMessage.System(const AContent: string): TLLMMessage; +begin + Result := Default(TLLMMessage); + Result.Role := lrSystem; + Result.Content := AContent; +end; + +class function TLLMMessage.Assistant(const AContent: string; + const AToolCalls: TArray): TLLMMessage; +begin + Result := Default(TLLMMessage); + Result.Role := lrAssistant; + Result.Content := AContent; + Result.ToolCalls := AToolCalls; +end; + +class function TLLMMessage.ToolResult(const AToolCallId, AContent: string): TLLMMessage; +begin + Result := Default(TLLMMessage); + Result.Role := lrToolResult; + Result.Content := AContent; + Result.ToolCallId := AToolCallId; +end; + +{ TAgentConfig } + +class function TAgentConfig.OpenAI(const AModel: string): TAgentConfig; +begin + Result := Default(TAgentConfig); + Result.ProviderString := 'openai:' + AModel; + Result.MaxTokens := 4096; + Result.MaxIterations := 15; + Result.SystemPrompt := DEFAULT_SYSTEM_PROMPT; +end; + +class function TAgentConfig.Anthropic(const AModel: string): TAgentConfig; +begin + Result := Default(TAgentConfig); + Result.ProviderString := 'anthropic:' + AModel; + Result.MaxTokens := 4096; + Result.MaxIterations := 15; + Result.SystemPrompt := DEFAULT_SYSTEM_PROMPT; +end; + +class function TAgentConfig.Ollama(const AModel: string): TAgentConfig; +begin + Result := Default(TAgentConfig); + Result.ProviderString := 'ollama:' + AModel; + Result.BaseUrl := 'http://localhost:11434'; + Result.MaxTokens := 4096; + Result.MaxIterations := 15; + Result.SystemPrompt := DEFAULT_SYSTEM_PROMPT; +end; + +end. diff --git a/Sources/AI/Agent/Dext.AI.Agent.Factory.pas b/Sources/AI/Agent/Dext.AI.Agent.Factory.pas new file mode 100644 index 00000000..9aa9ea90 --- /dev/null +++ b/Sources/AI/Agent/Dext.AI.Agent.Factory.pas @@ -0,0 +1,98 @@ +{***************************************************************************} +{ } +{ Dext Framework } +{ } +{ Dext.AI.Agent - Multi-Provider LLM Agent } +{ } +{***************************************************************************} +{ } +{ Description: } +{ TLLMFactory resolves the correct ILLMProvider from a config string, } +{ the same idea as LangChain's init_chat_model('provider:model'). } +{ } +{***************************************************************************} +unit Dext.AI.Agent.Factory; + +interface + +uses + Dext.AI.Agent.Contracts; + +type + TLLMFactory = class + public + // Cria o provider correto baseado em ProviderString + // Exemplos: + // 'openai:gpt-4o' + // 'openai:gpt-4-turbo' + // 'anthropic:claude-sonnet-4-6' + // 'anthropic:claude-haiku-4-5' + // 'ollama:llama3.2' + // 'ollama:mistral' + class function CreateProvider(const AConfig: TAgentConfig): ILLMProvider; + + // Parse do ProviderString → (ProviderName, ModelName) + class procedure ParseProviderString( + const AProviderString: string; + out AProvider, AModel: string + ); + end; + +implementation + +uses + System.SysUtils, + Dext.AI.Agent.Provider.OpenAI, + Dext.AI.Agent.Provider.Anthropic, + Dext.AI.Agent.Provider.Ollama; + +class procedure TLLMFactory.ParseProviderString( + const AProviderString: string; + out AProvider, AModel: string +); +var + Parts: TArray; +begin + Parts := AProviderString.Split([':']); + if Length(Parts) >= 1 then + AProvider := Parts[0].ToLower + else + AProvider := ''; + + if Length(Parts) >= 2 then + AModel := Parts[1] + else + AModel := ''; +end; + +class function TLLMFactory.CreateProvider(const AConfig: TAgentConfig): ILLMProvider; +var + ProviderName, ModelName: string; +begin + ParseProviderString(AConfig.ProviderString, ProviderName, ModelName); + + if ProviderName = 'openai' then + begin + if ModelName = '' then ModelName := 'gpt-4o'; + Result := TOpenAIProvider.Create(AConfig.ApiKey, ModelName, AConfig.MaxTokens); + end + else if ProviderName = 'anthropic' then + begin + if ModelName = '' then ModelName := 'claude-sonnet-4-6'; + Result := TAnthropicProvider.Create(AConfig.ApiKey, ModelName, AConfig.MaxTokens); + end + else if ProviderName = 'ollama' then + begin + if ModelName = '' then ModelName := 'llama3.2'; + var BaseUrl := AConfig.BaseUrl; + if BaseUrl = '' then BaseUrl := 'http://localhost:11434'; + Result := TOllamaProvider.Create(BaseUrl, ModelName, AConfig.MaxTokens); + end + else + raise ELLMProviderError.CreateFmt( + 'Provider desconhecido: "%s". Use: openai, anthropic ou ollama.', + [ProviderName] + ); +end; + +end. diff --git a/Sources/AI/Agent/Dext.AI.Agent.Observer.pas b/Sources/AI/Agent/Dext.AI.Agent.Observer.pas new file mode 100644 index 00000000..f2acae8b --- /dev/null +++ b/Sources/AI/Agent/Dext.AI.Agent.Observer.pas @@ -0,0 +1,78 @@ +{***************************************************************************} +{ } +{ Dext Framework } +{ } +{ Dext.AI.Agent - Multi-Provider LLM Agent } +{ } +{***************************************************************************} +{ } +{ Description: } +{ Console implementation of IAgentObserver - prints ReAct loop progress } +{ (iterations, tool calls, tool results, LLM responses) to stdout. } +{ } +{***************************************************************************} +unit Dext.AI.Agent.Observer; + +interface + +uses + Dext.AI.Agent.Contracts; + +type + TConsoleObserver = class(TInterfacedObject, IAgentObserver) + public + procedure OnIterationStart(AIteration: Integer); + procedure OnToolCall(const AToolName, AArgsJson: string); + procedure OnToolResult(const AToolName, AResult: string); + procedure OnLLMResponse(const AContent: string; AStopReason: TLLMStopReason); + procedure OnFinished(const AAnswer: string; AIterations: Integer); + end; + +implementation + +uses System.SysUtils; + +procedure TConsoleObserver.OnIterationStart(AIteration: Integer); +begin + Writeln; + Writeln(Format('[ITERAÇÃO %d] ─────────────────────────────', [AIteration])); +end; + +procedure TConsoleObserver.OnToolCall(const AToolName, AArgsJson: string); +var + Preview: string; +begin + Preview := AArgsJson; + if Length(Preview) > 80 then Preview := Preview.Substring(0, 80) + '...'; + Writeln(Format(' → Tool: %s', [AToolName])); + Writeln(Format(' Args: %s', [Preview])); +end; + +procedure TConsoleObserver.OnToolResult(const AToolName, AResult: string); +var + Preview: string; +begin + Preview := AResult; + if Length(Preview) > 120 then Preview := Preview.Substring(0, 120) + '...'; + Writeln(Format(' ← Resultado: %s', [Preview])); +end; + +procedure TConsoleObserver.OnLLMResponse(const AContent: string; AStopReason: TLLMStopReason); +begin + case AStopReason of + srToolUse: Writeln(' ⚡ LLM solicitou tool call'); + srEndTurn: Writeln(' ✓ LLM finalizou resposta'); + srMaxTokens: Writeln(' ⚠ Limite de tokens atingido'); + srError: Writeln(' ✗ Erro no LLM'); + end; +end; + +procedure TConsoleObserver.OnFinished(const AAnswer: string; AIterations: Integer); +begin + Writeln; + Writeln(Format('── RESPOSTA FINAL (%d iterações) ──────────', [AIterations])); + Writeln(AAnswer); + Writeln('──────────────────────────────────────────'); +end; + +end. diff --git a/Sources/AI/Agent/Dext.AI.Agent.Runner.pas b/Sources/AI/Agent/Dext.AI.Agent.Runner.pas new file mode 100644 index 00000000..696556c6 --- /dev/null +++ b/Sources/AI/Agent/Dext.AI.Agent.Runner.pas @@ -0,0 +1,321 @@ +{***************************************************************************} +{ } +{ Dext Framework } +{ } +{ Dext.AI.Agent - Multi-Provider LLM Agent } +{ } +{***************************************************************************} +{ } +{ Description: } +{ The ReAct loop. Provider-agnostic - talks only to ILLMProvider and to } +{ TMCPToolProvider subclasses, both already part of the Dext framework. } +{ } +{***************************************************************************} +unit Dext.AI.Agent.Runner; + +interface + +uses + Dext.AI.Agent.Contracts, + Dext.AI.MCP.Tools, + Dext.AI.MCP.Types, + Dext.AI.MCP.Protocol, + Dext.AI.MCP.Attributes, + Dext.Core.Reflection, + System.Rtti, + System.SysUtils, + System.JSON, + System.Generics.Collections; + +type + TAgentRunner = class + private + FProvider: ILLMProvider; + FConfig: TAgentConfig; + FObserver: IAgentObserver; + FProviders: TObjectList; + + function BuildToolSchemas: TArray; + function BuildInputSchema(AMethod: TRttiMethod): string; + function ExecuteTool(const AToolName, AArgsJson: string): string; + function ToolResultToText(const AResult: TMCPToolResult): string; + public + constructor Create( + AProvider: ILLMProvider; + const AConfig: TAgentConfig; + AObserver: IAgentObserver = nil + ); + destructor Destroy; override; + procedure RegisterProvider(AProvider: TMCPToolProvider); + function Run(const AUserInput: string): TAgentResult; + end; + +implementation + +{ TAgentRunner } + +constructor TAgentRunner.Create(AProvider: ILLMProvider; + const AConfig: TAgentConfig; AObserver: IAgentObserver); +begin + inherited Create; + FProvider := AProvider; + FConfig := AConfig; + FObserver := AObserver; + FProviders := TObjectList.Create(True); +end; + +destructor TAgentRunner.Destroy; +begin + FProviders.Free; + inherited; +end; + +procedure TAgentRunner.RegisterProvider(AProvider: TMCPToolProvider); +begin + FProviders.Add(AProvider); +end; + +function TAgentRunner.BuildInputSchema(AMethod: TRttiMethod): string; +var + Ctx: TRttiContext; + JSchema, JProps, JParam: TJSONObject; + JRequired: TJSONArray; + Attr: TCustomAttribute; + ParamAttr: MCPParamAttribute; +begin + Ctx := TRttiContext.Create; + try + JProps := TJSONObject.Create; + JRequired := TJSONArray.Create; + + for Attr in AMethod.GetAttributes do + if Attr is MCPParamAttribute then + begin + ParamAttr := MCPParamAttribute(Attr); + + JParam := TJSONObject.Create; + JParam.AddPair('description', ParamAttr.Description); + case ParamAttr.ParamType of + ptString: JParam.AddPair('type', 'string'); + ptInteger: JParam.AddPair('type', 'integer'); + ptNumber: JParam.AddPair('type', 'number'); + ptBoolean: JParam.AddPair('type', 'boolean'); + end; + JProps.AddPair(ParamAttr.Name, JParam); + + if ParamAttr.Required then + JRequired.Add(ParamAttr.Name); + end; + + JSchema := TJSONObject.Create; + try + JSchema.AddPair('type', 'object'); + JSchema.AddPair('properties', JProps); + if JRequired.Count > 0 then + JSchema.AddPair('required', JRequired) + else + JRequired.Free; + + Result := JSchema.ToJSON; + finally + JSchema.Free; + end; + finally + Ctx.Free; + end; +end; + +function TAgentRunner.BuildToolSchemas: TArray; +var + Ctx: TRttiContext; + Provider: TMCPToolProvider; + Method: TRttiMethod; + ToolAttr: MCPToolAttribute; + Schemas: TList; + Schema: TToolSchema; +begin + Ctx := TRttiContext.Create; + Schemas := TList.Create; + try + for Provider in FProviders do + for Method in Ctx.GetType(Provider.ClassType).GetMethods do + begin + ToolAttr := Method.GetAttribute; + if ToolAttr = nil then Continue; + + Schema := Default(TToolSchema); + Schema.Name := ToolAttr.Name; + Schema.Description := ToolAttr.Description; + Schema.InputSchema := BuildInputSchema(Method); + Schemas.Add(Schema); + end; + + Result := Schemas.ToArray; + finally + Schemas.Free; + Ctx.Free; + end; +end; + +function TAgentRunner.ToolResultToText(const AResult: TMCPToolResult): string; +var + Item: TMCPContent; + Parts: TStringBuilder; +begin + Parts := TStringBuilder.Create; + try + for Item in AResult.Content do + if Item.ContentType = mctText then + begin + if Parts.Length > 0 then + Parts.Append(sLineBreak); + Parts.Append(Item.TextValue); + end; + + Result := Parts.ToString; + if AResult.IsError then + Result := '[Error] ' + Result; + finally + Parts.Free; + end; +end; + +function TAgentRunner.ExecuteTool(const AToolName, AArgsJson: string): string; +var + Ctx: TRttiContext; + Provider: TMCPToolProvider; + RttiType: TRttiType; + Method: TRttiMethod; + ToolAttr: MCPToolAttribute; + JArgs: TJSONObject; + InvokeResult: TValue; +begin + Ctx := TRttiContext.Create; + try + JArgs := TJSONObject.ParseJSONValue(AArgsJson) as TJSONObject; + if JArgs = nil then + JArgs := TJSONObject.Create; + try + for Provider in FProviders do + begin + RttiType := Ctx.GetType(Provider.ClassType); + for Method in RttiType.GetMethods do + begin + ToolAttr := Method.GetAttribute; + if (ToolAttr = nil) or (ToolAttr.Name <> AToolName) then + Continue; + + try + Provider.BeforeCall(AToolName, JArgs); + InvokeResult := Method.Invoke(Provider, [TValue.From(JArgs)]); + Provider.AfterCall(AToolName); + Exit(ToolResultToText(InvokeResult.AsType)); + except + on E: Exception do + Exit('[Error] ' + E.Message); + end; + end; + end; + + Result := '[Error: Tool not found: ' + AToolName + ']'; + finally + JArgs.Free; + end; + finally + Ctx.Free; + end; +end; + +function TAgentRunner.Run(const AUserInput: string): TAgentResult; +var + Messages: TList; + Schemas: TArray; + Response: TLLMResponse; + Iteration: Integer; + TC: TLLMToolCall; + ToolResultText: string; +begin + Result := Default(TAgentResult); + + Messages := TList.Create; + try + if FConfig.SystemPrompt <> '' then + Messages.Add(TLLMMessage.System(FConfig.SystemPrompt)); + Messages.Add(TLLMMessage.User(AUserInput)); + + Schemas := BuildToolSchemas; + + for Iteration := 1 to FConfig.MaxIterations do + begin + if Assigned(FObserver) then + FObserver.OnIterationStart(Iteration); + + try + Response := FProvider.Complete(Messages.ToArray, Schemas); + except + on E: Exception do + begin + Result.Success := False; + Result.Iterations := Iteration; + Result.ErrorMsg := E.Message; + Exit; + end; + end; + + if Assigned(FObserver) then + FObserver.OnLLMResponse(Response.Content, Response.StopReason); + + case Response.StopReason of + srEndTurn: + begin + if Assigned(FObserver) then + FObserver.OnFinished(Response.Content, Iteration); + Result.FinalAnswer := Response.Content; + Result.Success := True; + Result.Iterations := Iteration; + Exit; + end; + + srToolUse: + begin + Messages.Add(TLLMMessage.Assistant(Response.Content, Response.ToolCalls)); + + for TC in Response.ToolCalls do + begin + if Assigned(FObserver) then + FObserver.OnToolCall(TC.Name, TC.ArgsJson); + + ToolResultText := ExecuteTool(TC.Name, TC.ArgsJson); + + if Assigned(FObserver) then + FObserver.OnToolResult(TC.Name, ToolResultText); + + Messages.Add(TLLMMessage.ToolResult(TC.Id, ToolResultText)); + end; + end; + + srMaxTokens: + begin + Result.FinalAnswer := Response.Content; + Result.Success := False; + Result.Iterations := Iteration; + Result.ErrorMsg := 'Limite de tokens atingido'; + Exit; + end; + else + begin + Result.Success := False; + Result.ErrorMsg := 'Erro reportado pelo provider (srError)'; + Exit; + end; + end; + end; + + Result.Success := False; + Result.ErrorMsg := 'Limite de iterações atingido'; + finally + Messages.Free; + end; +end; + +end. diff --git a/Sources/AI/Agent/Providers/Dext.AI.Agent.Provider.Anthropic.pas b/Sources/AI/Agent/Providers/Dext.AI.Agent.Provider.Anthropic.pas new file mode 100644 index 00000000..7d1c0fca --- /dev/null +++ b/Sources/AI/Agent/Providers/Dext.AI.Agent.Provider.Anthropic.pas @@ -0,0 +1,323 @@ +{***************************************************************************} +{ } +{ Dext Framework } +{ } +{ Dext.AI.Agent - Multi-Provider LLM Agent } +{ } +{***************************************************************************} +{ } +{ Description: } +{ ILLMProvider implementation for the Anthropic Messages API. } +{ POST https://api.anthropic.com/v1/messages } +{ } +{ Anthropic requires every tool_result produced in reaction to a single } +{ assistant turn to be sent back as ONE user message whose content is } +{ an array of tool_result blocks. The Runner appends one lrToolResult } +{ TLLMMessage per tool call, so this provider coalesces any run of } +{ consecutive lrToolResult messages into a single user message when } +{ building the request body. } +{ } +{***************************************************************************} +unit Dext.AI.Agent.Provider.Anthropic; + +interface + +uses + System.SysUtils, + System.Classes, + System.JSON, + System.NetConsts, + System.Net.HttpClient, + System.Net.URLClient, + System.Generics.Collections, + Dext.AI.Agent.Contracts; + +type + TAnthropicProvider = class(TInterfacedObject, ILLMProvider) + private + FApiKey: string; + FModel: string; + FMaxTokens: Integer; + FEndpoint: string; + + function BuildRequestBody(const AMessages: TArray; + const ATools: TArray): TJSONObject; + function BuildToolJSON(const ATool: TToolSchema): TJSONObject; + function ParseResponse(const ABody: string): TLLMResponse; + function MapStopReason(const AReason: string): TLLMStopReason; + public + constructor Create(const AApiKey, AModel: string; AMaxTokens: Integer); + + function Complete( + const AMessages: TArray; + const ATools: TArray + ): TLLMResponse; + function ProviderName: string; + function ModelName: string; + end; + +implementation + +{ TAnthropicProvider } + +constructor TAnthropicProvider.Create(const AApiKey, AModel: string; AMaxTokens: Integer); +begin + inherited Create; + FApiKey := AApiKey; + FModel := AModel; + FMaxTokens := AMaxTokens; + FEndpoint := 'https://api.anthropic.com/v1/messages'; +end; + +function TAnthropicProvider.ProviderName: string; +begin + Result := 'anthropic'; +end; + +function TAnthropicProvider.ModelName: string; +begin + Result := FModel; +end; + +function TAnthropicProvider.BuildToolJSON(const ATool: TToolSchema): TJSONObject; +var + Schema: TJSONValue; +begin + Result := TJSONObject.Create; + Result.AddPair('name', ATool.Name); + Result.AddPair('description', ATool.Description); + + Schema := TJSONObject.ParseJSONValue(ATool.InputSchema); + if Schema = nil then + Schema := TJSONObject.Create; + Result.AddPair('input_schema', Schema); +end; + +function TAnthropicProvider.BuildRequestBody(const AMessages: TArray; + const ATools: TArray): TJSONObject; +var + MsgsArr, ToolsArr: TJSONArray; + ContentArr: TJSONArray; + MsgObj, ContentBlock: TJSONObject; + I, J: Integer; + Msg: TLLMMessage; + TC: TLLMToolCall; + Tool: TToolSchema; + ArgsVal: TJSONValue; +begin + Result := TJSONObject.Create; + Result.AddPair('model', FModel); + Result.AddPair('max_tokens', TJSONNumber.Create(FMaxTokens)); + + MsgsArr := TJSONArray.Create; + + I := 0; + while I < Length(AMessages) do + begin + Msg := AMessages[I]; + + case Msg.Role of + lrSystem: + begin + Result.AddPair('system', Msg.Content); + Inc(I); + end; + + lrUser: + begin + MsgObj := TJSONObject.Create; + MsgObj.AddPair('role', 'user'); + MsgObj.AddPair('content', Msg.Content); + MsgsArr.Add(MsgObj); + Inc(I); + end; + + lrAssistant: + begin + MsgObj := TJSONObject.Create; + MsgObj.AddPair('role', 'assistant'); + ContentArr := TJSONArray.Create; + + if Msg.Content <> '' then + begin + ContentBlock := TJSONObject.Create; + ContentBlock.AddPair('type', 'text'); + ContentBlock.AddPair('text', Msg.Content); + ContentArr.Add(ContentBlock); + end; + + for TC in Msg.ToolCalls do + begin + ContentBlock := TJSONObject.Create; + ContentBlock.AddPair('type', 'tool_use'); + ContentBlock.AddPair('id', TC.Id); + ContentBlock.AddPair('name', TC.Name); + ArgsVal := TJSONObject.ParseJSONValue(TC.ArgsJson); + if ArgsVal = nil then + ArgsVal := TJSONObject.Create; + ContentBlock.AddPair('input', ArgsVal); + ContentArr.Add(ContentBlock); + end; + + MsgObj.AddPair('content', ContentArr); + MsgsArr.Add(MsgObj); + Inc(I); + end; + + lrToolResult: + begin + // Coalesce this run of consecutive tool-result messages into a + // single {"role":"user","content":[tool_result, tool_result, ...]} + MsgObj := TJSONObject.Create; + MsgObj.AddPair('role', 'user'); + ContentArr := TJSONArray.Create; + + J := I; + while (J < Length(AMessages)) and (AMessages[J].Role = lrToolResult) do + begin + ContentBlock := TJSONObject.Create; + ContentBlock.AddPair('type', 'tool_result'); + ContentBlock.AddPair('tool_use_id', AMessages[J].ToolCallId); + ContentBlock.AddPair('content', AMessages[J].Content); + ContentArr.Add(ContentBlock); + Inc(J); + end; + + MsgObj.AddPair('content', ContentArr); + MsgsArr.Add(MsgObj); + I := J; + end; + else + Inc(I); + end; + end; + + Result.AddPair('messages', MsgsArr); + + if Length(ATools) > 0 then + begin + ToolsArr := TJSONArray.Create; + for Tool in ATools do + ToolsArr.Add(BuildToolJSON(Tool)); + Result.AddPair('tools', ToolsArr); + end; +end; + +function TAnthropicProvider.MapStopReason(const AReason: string): TLLMStopReason; +begin + if AReason = 'end_turn' then + Result := srEndTurn + else if AReason = 'tool_use' then + Result := srToolUse + else if AReason = 'max_tokens' then + Result := srMaxTokens + else + Result := srError; +end; + +function TAnthropicProvider.ParseResponse(const ABody: string): TLLMResponse; +var + Root, Usage, Block: TJSONObject; + ContentArr: TJSONArray; + I: Integer; + BlockType: string; + TextBuf: TStringBuilder; + ToolCalls: TList; + TC: TLLMToolCall; +begin + Result := Default(TLLMResponse); + + Root := TJSONObject.ParseJSONValue(ABody) as TJSONObject; + if Root = nil then + raise ELLMProviderError.CreateFmt('Anthropic: resposta inválida: %s', [ABody]); + try + ContentArr := Root.GetValue('content', nil); + if ContentArr = nil then + raise ELLMProviderError.CreateFmt('Anthropic: resposta sem content: %s', [ABody]); + + TextBuf := TStringBuilder.Create; + ToolCalls := TList.Create; + try + for I := 0 to ContentArr.Count - 1 do + begin + Block := ContentArr.Items[I] as TJSONObject; + BlockType := Block.GetValue('type', ''); + + if BlockType = 'text' then + TextBuf.Append(Block.GetValue('text', '')) + else if BlockType = 'tool_use' then + begin + TC := Default(TLLMToolCall); + TC.Id := Block.GetValue('id', ''); + TC.Name := Block.GetValue('name', ''); + if Block.GetValue('input') <> nil then + TC.ArgsJson := Block.GetValue('input').ToJSON + else + TC.ArgsJson := '{}'; + ToolCalls.Add(TC); + end; + end; + + Result.Content := TextBuf.ToString; + Result.ToolCalls := ToolCalls.ToArray; + finally + TextBuf.Free; + ToolCalls.Free; + end; + + Result.StopReason := MapStopReason(Root.GetValue('stop_reason', '')); + + Usage := Root.GetValue('usage', nil); + if Usage <> nil then + begin + Result.InputTokens := Usage.GetValue('input_tokens', 0); + Result.OutputTokens := Usage.GetValue('output_tokens', 0); + end; + finally + Root.Free; + end; +end; + +function TAnthropicProvider.Complete(const AMessages: TArray; + const ATools: TArray): TLLMResponse; +var + HttpClient: THTTPClient; + Body: TJSONObject; + Stream: TStringStream; + Response: IHTTPResponse; +begin + if FApiKey = '' then + raise ELLMProviderError.Create('Anthropic: API key não configurada.'); + + HttpClient := THTTPClient.Create; + try + HttpClient.ConnectionTimeout := 120000; + HttpClient.ResponseTimeout := 120000; + HttpClient.CustomHeaders['x-api-key'] := FApiKey; + HttpClient.CustomHeaders['anthropic-version'] := '2023-06-01'; + HttpClient.ContentType := 'application/json'; + + Body := BuildRequestBody(AMessages, ATools); + try + Stream := TStringStream.Create(Body.ToJSON, TEncoding.UTF8); + try + Response := HttpClient.Post(FEndpoint, Stream, nil, + [TNetHeader.Create('Content-Type', 'application/json')]); + finally + Stream.Free; + end; + finally + Body.Free; + end; + + if Response.StatusCode <> 200 then + raise ELLMProviderError.CreateFmt('Anthropic HTTP %d: %s', + [Response.StatusCode, Response.ContentAsString(TEncoding.UTF8)]); + + Result := ParseResponse(Response.ContentAsString(TEncoding.UTF8)); + finally + HttpClient.Free; + end; +end; + +end. diff --git a/Sources/AI/Agent/Providers/Dext.AI.Agent.Provider.Ollama.pas b/Sources/AI/Agent/Providers/Dext.AI.Agent.Provider.Ollama.pas new file mode 100644 index 00000000..a4acce09 --- /dev/null +++ b/Sources/AI/Agent/Providers/Dext.AI.Agent.Provider.Ollama.pas @@ -0,0 +1,269 @@ +{***************************************************************************} +{ } +{ Dext Framework } +{ } +{ Dext.AI.Agent - Multi-Provider LLM Agent } +{ } +{***************************************************************************} +{ } +{ Description: } +{ ILLMProvider implementation for a local Ollama server (OpenAI-style } +{ chat endpoint, no API key required). } +{ POST /api/chat } +{ } +{***************************************************************************} +unit Dext.AI.Agent.Provider.Ollama; + +interface + +uses + System.SysUtils, + System.Classes, + System.JSON, + System.NetConsts, + System.Net.HttpClient, + System.Net.URLClient, + System.Generics.Collections, + Dext.AI.Agent.Contracts; + +type + TOllamaProvider = class(TInterfacedObject, ILLMProvider) + private + FBaseUrl: string; + FModel: string; + FMaxTokens: Integer; + + function BuildRequestBody(const AMessages: TArray; + const ATools: TArray): TJSONObject; + function BuildMessageJSON(const AMessage: TLLMMessage): TJSONObject; + function BuildToolJSON(const ATool: TToolSchema): TJSONObject; + function ParseResponse(const ABody: string): TLLMResponse; + function MapDoneReason(const AReason: string): TLLMStopReason; + public + constructor Create(const ABaseUrl, AModel: string; AMaxTokens: Integer); + + function Complete( + const AMessages: TArray; + const ATools: TArray + ): TLLMResponse; + function ProviderName: string; + function ModelName: string; + end; + +implementation + +{ TOllamaProvider } + +constructor TOllamaProvider.Create(const ABaseUrl, AModel: string; AMaxTokens: Integer); +begin + inherited Create; + FBaseUrl := ABaseUrl.TrimRight(['/']); + FModel := AModel; + FMaxTokens := AMaxTokens; +end; + +function TOllamaProvider.ProviderName: string; +begin + Result := 'ollama'; +end; + +function TOllamaProvider.ModelName: string; +begin + Result := FModel; +end; + +function TOllamaProvider.BuildMessageJSON(const AMessage: TLLMMessage): TJSONObject; +var + ToolCallsArr: TJSONArray; + TC: TLLMToolCall; + TCObj, FnObj: TJSONObject; +begin + Result := TJSONObject.Create; + case AMessage.Role of + lrSystem: + begin + Result.AddPair('role', 'system'); + Result.AddPair('content', AMessage.Content); + end; + lrUser: + begin + Result.AddPair('role', 'user'); + Result.AddPair('content', AMessage.Content); + end; + lrAssistant: + begin + Result.AddPair('role', 'assistant'); + Result.AddPair('content', AMessage.Content); + + if Length(AMessage.ToolCalls) > 0 then + begin + ToolCallsArr := TJSONArray.Create; + for TC in AMessage.ToolCalls do + begin + TCObj := TJSONObject.Create; + FnObj := TJSONObject.Create; + FnObj.AddPair('name', TC.Name); + FnObj.AddPair('arguments', TC.ArgsJson); + TCObj.AddPair('function', FnObj); + ToolCallsArr.Add(TCObj); + end; + Result.AddPair('tool_calls', ToolCallsArr); + end; + end; + lrToolResult: + begin + Result.AddPair('role', 'tool'); + Result.AddPair('tool_call_id', AMessage.ToolCallId); + Result.AddPair('content', AMessage.Content); + end; + end; +end; + +function TOllamaProvider.BuildToolJSON(const ATool: TToolSchema): TJSONObject; +var + FnObj: TJSONObject; + Params: TJSONValue; +begin + Result := TJSONObject.Create; + Result.AddPair('type', 'function'); + + FnObj := TJSONObject.Create; + FnObj.AddPair('name', ATool.Name); + FnObj.AddPair('description', ATool.Description); + + Params := TJSONObject.ParseJSONValue(ATool.InputSchema); + if Params = nil then + Params := TJSONObject.Create; + FnObj.AddPair('parameters', Params); + + Result.AddPair('function', FnObj); +end; + +function TOllamaProvider.BuildRequestBody(const AMessages: TArray; + const ATools: TArray): TJSONObject; +var + MsgsArr, ToolsArr: TJSONArray; + Msg: TLLMMessage; + Tool: TToolSchema; +begin + Result := TJSONObject.Create; + Result.AddPair('model', FModel); + Result.AddPair('stream', TJSONBool.Create(False)); + + MsgsArr := TJSONArray.Create; + for Msg in AMessages do + MsgsArr.Add(BuildMessageJSON(Msg)); + Result.AddPair('messages', MsgsArr); + + if Length(ATools) > 0 then + begin + ToolsArr := TJSONArray.Create; + for Tool in ATools do + ToolsArr.Add(BuildToolJSON(Tool)); + Result.AddPair('tools', ToolsArr); + end; +end; + +function TOllamaProvider.MapDoneReason(const AReason: string): TLLMStopReason; +begin + if AReason = 'stop' then + Result := srEndTurn + else if AReason = 'tool_calls' then + Result := srToolUse + else + Result := srError; +end; + +function TOllamaProvider.ParseResponse(const ABody: string): TLLMResponse; +var + Root, Message, TCObj, FnObj: TJSONObject; + ToolCallsArr: TJSONArray; + ToolCalls: TArray; + I: Integer; + TC: TLLMToolCall; + ArgsVal: TJSONValue; +begin + Result := Default(TLLMResponse); + + Root := TJSONObject.ParseJSONValue(ABody) as TJSONObject; + if Root = nil then + raise ELLMProviderError.CreateFmt('Ollama: resposta inválida: %s', [ABody]); + try + Message := Root.GetValue('message', nil); + if Message = nil then + raise ELLMProviderError.CreateFmt('Ollama: resposta sem message: %s', [ABody]); + + Result.Content := Message.GetValue('content', ''); + + // 'tool_calls' is absent on a plain-text final answer - GetValue with a + // default is required here, the 1-arg overload raises EJSONException instead + // of returning nil when the key is missing. + ToolCallsArr := Message.GetValue('tool_calls', nil); + if ToolCallsArr <> nil then + begin + SetLength(ToolCalls, ToolCallsArr.Count); + for I := 0 to ToolCallsArr.Count - 1 do + begin + TCObj := ToolCallsArr.Items[I] as TJSONObject; + FnObj := TCObj.GetValue('function', nil); + TC := Default(TLLMToolCall); + TC.Id := 'ollama-call-' + IntToStr(I); + TC.Name := FnObj.GetValue('name', ''); + + ArgsVal := FnObj.GetValue('arguments'); + if ArgsVal <> nil then + TC.ArgsJson := ArgsVal.ToJSON + else + TC.ArgsJson := '{}'; + + ToolCalls[I] := TC; + end; + Result.ToolCalls := ToolCalls; + end; + + Result.StopReason := MapDoneReason(Root.GetValue('done_reason', '')); + Result.InputTokens := 0; + Result.OutputTokens := 0; + finally + Root.Free; + end; +end; + +function TOllamaProvider.Complete(const AMessages: TArray; + const ATools: TArray): TLLMResponse; +var + HttpClient: THTTPClient; + Body: TJSONObject; + Stream: TStringStream; + Response: IHTTPResponse; +begin + HttpClient := THTTPClient.Create; + try + HttpClient.ConnectionTimeout := 120000; + HttpClient.ResponseTimeout := 120000; + HttpClient.ContentType := 'application/json'; + + Body := BuildRequestBody(AMessages, ATools); + try + Stream := TStringStream.Create(Body.ToJSON, TEncoding.UTF8); + try + Response := HttpClient.Post(FBaseUrl + '/api/chat', Stream, nil, + [TNetHeader.Create('Content-Type', 'application/json')]); + finally + Stream.Free; + end; + finally + Body.Free; + end; + + if Response.StatusCode <> 200 then + raise ELLMProviderError.CreateFmt('Ollama HTTP %d: %s', + [Response.StatusCode, Response.ContentAsString(TEncoding.UTF8)]); + + Result := ParseResponse(Response.ContentAsString(TEncoding.UTF8)); + finally + HttpClient.Free; + end; +end; + +end. diff --git a/Sources/AI/Agent/Providers/Dext.AI.Agent.Provider.OpenAI.pas b/Sources/AI/Agent/Providers/Dext.AI.Agent.Provider.OpenAI.pas new file mode 100644 index 00000000..829786c8 --- /dev/null +++ b/Sources/AI/Agent/Providers/Dext.AI.Agent.Provider.OpenAI.pas @@ -0,0 +1,291 @@ +{***************************************************************************} +{ } +{ Dext Framework } +{ } +{ Dext.AI.Agent - Multi-Provider LLM Agent } +{ } +{***************************************************************************} +{ } +{ Description: } +{ ILLMProvider implementation for OpenAI's Chat Completions API. } +{ POST https://api.openai.com/v1/chat/completions } +{ } +{***************************************************************************} +unit Dext.AI.Agent.Provider.OpenAI; + +interface + +uses + System.SysUtils, + System.Classes, + System.JSON, + System.NetConsts, + System.Net.HttpClient, + System.Net.URLClient, + System.Generics.Collections, + Dext.AI.Agent.Contracts; + +type + TOpenAIProvider = class(TInterfacedObject, ILLMProvider) + private + FApiKey: string; + FModel: string; + FMaxTokens: Integer; + FEndpoint: string; + + function BuildRequestBody(const AMessages: TArray; + const ATools: TArray): TJSONObject; + function BuildMessageJSON(const AMessage: TLLMMessage): TJSONObject; + function BuildToolJSON(const ATool: TToolSchema): TJSONObject; + function ParseResponse(const ABody: string): TLLMResponse; + function MapFinishReason(const AReason: string): TLLMStopReason; + public + constructor Create(const AApiKey, AModel: string; AMaxTokens: Integer); + + function Complete( + const AMessages: TArray; + const ATools: TArray + ): TLLMResponse; + function ProviderName: string; + function ModelName: string; + end; + +implementation + +{ TOpenAIProvider } + +constructor TOpenAIProvider.Create(const AApiKey, AModel: string; AMaxTokens: Integer); +begin + inherited Create; + FApiKey := AApiKey; + FModel := AModel; + FMaxTokens := AMaxTokens; + FEndpoint := 'https://api.openai.com/v1/chat/completions'; +end; + +function TOpenAIProvider.ProviderName: string; +begin + Result := 'openai'; +end; + +function TOpenAIProvider.ModelName: string; +begin + Result := FModel; +end; + +function TOpenAIProvider.BuildMessageJSON(const AMessage: TLLMMessage): TJSONObject; +var + ToolCallsArr: TJSONArray; + TC: TLLMToolCall; + TCObj, FnObj: TJSONObject; +begin + Result := TJSONObject.Create; + case AMessage.Role of + lrSystem: + begin + Result.AddPair('role', 'system'); + Result.AddPair('content', AMessage.Content); + end; + lrUser: + begin + Result.AddPair('role', 'user'); + Result.AddPair('content', AMessage.Content); + end; + lrAssistant: + begin + Result.AddPair('role', 'assistant'); + if Length(AMessage.ToolCalls) > 0 then + begin + if AMessage.Content <> '' then + Result.AddPair('content', AMessage.Content) + else + Result.AddPair('content', TJSONNull.Create); + + ToolCallsArr := TJSONArray.Create; + for TC in AMessage.ToolCalls do + begin + TCObj := TJSONObject.Create; + TCObj.AddPair('id', TC.Id); + TCObj.AddPair('type', 'function'); + FnObj := TJSONObject.Create; + FnObj.AddPair('name', TC.Name); + FnObj.AddPair('arguments', TC.ArgsJson); + TCObj.AddPair('function', FnObj); + ToolCallsArr.Add(TCObj); + end; + Result.AddPair('tool_calls', ToolCallsArr); + end + else + Result.AddPair('content', AMessage.Content); + end; + lrToolResult: + begin + Result.AddPair('role', 'tool'); + Result.AddPair('tool_call_id', AMessage.ToolCallId); + Result.AddPair('content', AMessage.Content); + end; + end; +end; + +function TOpenAIProvider.BuildToolJSON(const ATool: TToolSchema): TJSONObject; +var + FnObj: TJSONObject; + Params: TJSONValue; +begin + Result := TJSONObject.Create; + Result.AddPair('type', 'function'); + + FnObj := TJSONObject.Create; + FnObj.AddPair('name', ATool.Name); + FnObj.AddPair('description', ATool.Description); + + Params := TJSONObject.ParseJSONValue(ATool.InputSchema); + if Params = nil then + Params := TJSONObject.Create; + FnObj.AddPair('parameters', Params); + + Result.AddPair('function', FnObj); +end; + +function TOpenAIProvider.BuildRequestBody(const AMessages: TArray; + const ATools: TArray): TJSONObject; +var + MsgsArr, ToolsArr: TJSONArray; + Msg: TLLMMessage; + Tool: TToolSchema; +begin + Result := TJSONObject.Create; + Result.AddPair('model', FModel); + Result.AddPair('max_tokens', TJSONNumber.Create(FMaxTokens)); + + MsgsArr := TJSONArray.Create; + for Msg in AMessages do + MsgsArr.Add(BuildMessageJSON(Msg)); + Result.AddPair('messages', MsgsArr); + + if Length(ATools) > 0 then + begin + ToolsArr := TJSONArray.Create; + for Tool in ATools do + ToolsArr.Add(BuildToolJSON(Tool)); + Result.AddPair('tools', ToolsArr); + end; +end; + +function TOpenAIProvider.MapFinishReason(const AReason: string): TLLMStopReason; +begin + if AReason = 'stop' then + Result := srEndTurn + else if AReason = 'tool_calls' then + Result := srToolUse + else if AReason = 'length' then + Result := srMaxTokens + else + Result := srError; +end; + +function TOpenAIProvider.ParseResponse(const ABody: string): TLLMResponse; +var + Root, Choice, Message, Usage, FnObj, TCObj: TJSONObject; + Choices, ToolCallsArr: TJSONArray; + FinishReason: string; + ToolCalls: TArray; + I: Integer; + TC: TLLMToolCall; + ContentVal: TJSONValue; +begin + Result := Default(TLLMResponse); + + Root := TJSONObject.ParseJSONValue(ABody) as TJSONObject; + if Root = nil then + raise ELLMProviderError.CreateFmt('OpenAI: resposta inválida: %s', [ABody]); + try + Choices := Root.GetValue('choices', nil); + if (Choices = nil) or (Choices.Count = 0) then + raise ELLMProviderError.CreateFmt('OpenAI: resposta sem choices: %s', [ABody]); + + Choice := Choices.Items[0] as TJSONObject; + FinishReason := Choice.GetValue('finish_reason', ''); + Message := Choice.GetValue('message', nil); + if Message = nil then + raise ELLMProviderError.CreateFmt('OpenAI: choice sem message: %s', [ABody]); + + ContentVal := Message.GetValue('content'); + if (ContentVal <> nil) and not (ContentVal is TJSONNull) then + Result.Content := ContentVal.Value; + + // 'tool_calls' is absent on a plain-text final answer - GetValue with a + // default is required here, the 1-arg overload raises EJSONException instead + // of returning nil when the key is missing. + ToolCallsArr := Message.GetValue('tool_calls', nil); + if ToolCallsArr <> nil then + begin + SetLength(ToolCalls, ToolCallsArr.Count); + for I := 0 to ToolCallsArr.Count - 1 do + begin + TCObj := ToolCallsArr.Items[I] as TJSONObject; + FnObj := TCObj.GetValue('function', nil); + TC := Default(TLLMToolCall); + TC.Id := TCObj.GetValue('id', ''); + TC.Name := FnObj.GetValue('name', ''); + TC.ArgsJson := FnObj.GetValue('arguments', '{}'); + ToolCalls[I] := TC; + end; + Result.ToolCalls := ToolCalls; + end; + + Result.StopReason := MapFinishReason(FinishReason); + + Usage := Root.GetValue('usage', nil); + if Usage <> nil then + begin + Result.InputTokens := Usage.GetValue('prompt_tokens', 0); + Result.OutputTokens := Usage.GetValue('completion_tokens', 0); + end; + finally + Root.Free; + end; +end; + +function TOpenAIProvider.Complete(const AMessages: TArray; + const ATools: TArray): TLLMResponse; +var + HttpClient: THTTPClient; + Body: TJSONObject; + Stream: TStringStream; + Response: IHTTPResponse; +begin + if FApiKey = '' then + raise ELLMProviderError.Create('OpenAI: API key não configurada.'); + + HttpClient := THTTPClient.Create; + try + HttpClient.ConnectionTimeout := 120000; + HttpClient.ResponseTimeout := 120000; + HttpClient.CustomHeaders['Authorization'] := 'Bearer ' + FApiKey; + HttpClient.ContentType := 'application/json'; + + Body := BuildRequestBody(AMessages, ATools); + try + Stream := TStringStream.Create(Body.ToJSON, TEncoding.UTF8); + try + Response := HttpClient.Post(FEndpoint, Stream, nil, + [TNetHeader.Create('Content-Type', 'application/json')]); + finally + Stream.Free; + end; + finally + Body.Free; + end; + + if Response.StatusCode <> 200 then + raise ELLMProviderError.CreateFmt('OpenAI HTTP %d: %s', + [Response.StatusCode, Response.ContentAsString(TEncoding.UTF8)]); + + Result := ParseResponse(Response.ContentAsString(TEncoding.UTF8)); + finally + HttpClient.Free; + end; +end; + +end. diff --git a/Sources/AI/Graph/Dext.AI.Graph.Checkpointer.pas b/Sources/AI/Graph/Dext.AI.Graph.Checkpointer.pas new file mode 100644 index 00000000..08711a89 --- /dev/null +++ b/Sources/AI/Graph/Dext.AI.Graph.Checkpointer.pas @@ -0,0 +1,152 @@ +{***************************************************************************} +{ } +{ Dext Framework } +{ } +{ Dext.AI.Graph - Orquestração de agentes estilo LangGraph } +{ } +{***************************************************************************} +{ } +{ Description: } +{ Checkpointers em memória (MemorySaver) e em arquivo JSON. } +{ } +{***************************************************************************} +unit Dext.AI.Graph.Checkpointer; + +interface + +uses + Dext.AI.Graph.Contracts, + System.Generics.Collections, + System.SysUtils; + +type + TMemoryCheckpointer = class(TInterfacedObject, ICheckpointer) + private + FStore: TDictionary; + public + constructor Create; + destructor Destroy; override; + procedure Save(const AThreadId: string; const AStateJson: string); + function Load(const AThreadId: string): string; + function Exists(const AThreadId: string): Boolean; + procedure Delete(const AThreadId: string); + end; + + TFileCheckpointer = class(TInterfacedObject, ICheckpointer) + private + FBasePath: string; + function FilePath(const AThreadId: string): string; + function SanitizeId(const AThreadId: string): string; + public + constructor Create(const ABasePath: string = ''); + procedure Save(const AThreadId: string; const AStateJson: string); + function Load(const AThreadId: string): string; + function Exists(const AThreadId: string): Boolean; + procedure Delete(const AThreadId: string); + end; + +implementation + +uses + System.IOUtils; + +{ TMemoryCheckpointer } + +constructor TMemoryCheckpointer.Create; +begin + inherited Create; + FStore := TDictionary.Create; +end; + +destructor TMemoryCheckpointer.Destroy; +begin + FStore.Free; + inherited; +end; + +procedure TMemoryCheckpointer.Save(const AThreadId: string; const AStateJson: string); +begin + FStore.AddOrSetValue(AThreadId, AStateJson); +end; + +function TMemoryCheckpointer.Load(const AThreadId: string): string; +begin + if not FStore.TryGetValue(AThreadId, Result) then + raise EGraphError.CreateFmt('Checkpoint não encontrado: %s', [AThreadId]); +end; + +function TMemoryCheckpointer.Exists(const AThreadId: string): Boolean; +begin + Result := FStore.ContainsKey(AThreadId); +end; + +procedure TMemoryCheckpointer.Delete(const AThreadId: string); +begin + FStore.Remove(AThreadId); +end; + +{ TFileCheckpointer } + +constructor TFileCheckpointer.Create(const ABasePath: string); +begin + inherited Create; + if ABasePath = '' then + FBasePath := TPath.Combine(TPath.GetTempPath, 'dext-ai-graph') + else + FBasePath := ABasePath; +end; + +function TFileCheckpointer.SanitizeId(const AThreadId: string): string; +var + I: Integer; + C: Char; +begin + Result := ''; + for I := 1 to Length(AThreadId) do + begin + C := AThreadId[I]; + if CharInSet(C, ['A'..'Z', 'a'..'z', '0'..'9', '-', '_']) then + Result := Result + C + else + Result := Result + '_'; + end; + if Result = '' then + Result := 'thread'; +end; + +function TFileCheckpointer.FilePath(const AThreadId: string): string; +begin + Result := TPath.Combine(FBasePath, SanitizeId(AThreadId) + '.json'); +end; + +procedure TFileCheckpointer.Save(const AThreadId: string; const AStateJson: string); +begin + TDirectory.CreateDirectory(FBasePath); + TFile.WriteAllText(FilePath(AThreadId), AStateJson, TEncoding.UTF8); +end; + +function TFileCheckpointer.Load(const AThreadId: string): string; +var + Path: string; +begin + Path := FilePath(AThreadId); + if not TFile.Exists(Path) then + raise EGraphError.CreateFmt('Checkpoint não encontrado: %s', [AThreadId]); + Result := TFile.ReadAllText(Path, TEncoding.UTF8); +end; + +function TFileCheckpointer.Exists(const AThreadId: string): Boolean; +begin + Result := TFile.Exists(FilePath(AThreadId)); +end; + +procedure TFileCheckpointer.Delete(const AThreadId: string); +var + Path: string; +begin + Path := FilePath(AThreadId); + if TFile.Exists(Path) then + TFile.Delete(Path); +end; + +end. diff --git a/Sources/AI/Graph/Dext.AI.Graph.Compiled.pas b/Sources/AI/Graph/Dext.AI.Graph.Compiled.pas new file mode 100644 index 00000000..41607fbc --- /dev/null +++ b/Sources/AI/Graph/Dext.AI.Graph.Compiled.pas @@ -0,0 +1,359 @@ +{***************************************************************************} +{ } +{ Dext Framework } +{ } +{ Dext.AI.Graph - Orquestração de agentes estilo LangGraph } +{ } +{***************************************************************************} +{ } +{ Description: } +{ TCompiledAgent — resultado de TAgentGraph.Compile(). } +{ Executa o grafo nó a nó, gerenciando estado e checkpoints. } +{ } +{***************************************************************************} +unit Dext.AI.Graph.Compiled; + +interface + +uses + System.SysUtils, + System.Generics.Collections, + Dext.AI.Graph.Contracts, + Dext.AI.Graph.State, + Dext.AI.Graph.Edge, + Dext.AI.Graph.Graph, + Dext.AI.Graph.Checkpointer, + Dext.AI.Agent.Contracts; + +type + TCompiledAgent = class(TInterfacedObject, ICompiledAgent) + private + FNodes: TDictionary; + FEdges: TList; + FEntryPoint: string; + FInterruptBefore: TArray; + FContext: TNodeContext; + FCheckpointer: ICheckpointer; + FMaxIterations: Integer; + FHeldState: TAgentState; + + function ExecuteNode( + const ANodeName: string; + const AState: TAgentState + ): TAgentState; + + function ResolveNextNode( + const ACurrentNode: string; + const AState: TAgentState + ): string; + + function ShouldInterrupt(const ANodeName: string): Boolean; + function GenerateThreadId: string; + procedure CheckpointSave(const AThreadId: string; AState: TAgentState); + function CheckpointLoad(const AThreadId: string): TAgentState; + function ExecuteLoop(AState: TAgentState; ASkipFirstInterrupt: Boolean): TGraphRunResult; + function DescribePending(AState: TAgentState; const ANode: string): string; + public + constructor Create( + ANodes: TDictionary; + AEdges: TList; + const AEntryPoint: string; + const AInterruptBefore: TArray; + const AContext: TNodeContext; + ACheckpointer: ICheckpointer; + AMaxIterations: Integer + ); + destructor Destroy; override; + + function Run( + const AInput: string; + const AThreadId: string = '' + ): TGraphRunResult; + + function Resume(const AThreadId: string): TGraphRunResult; + procedure Cancel(const AThreadId: string); + function GetState(const AThreadId: string): TObject; + end; + +implementation + +function ReplaceState(var Current: TAgentState; NewState: TAgentState): TAgentState; +begin + if (Current <> nil) and (Current <> NewState) then + Current.Free; + Current := NewState; + Result := Current; +end; + +{ TCompiledAgent } + +constructor TCompiledAgent.Create( + ANodes: TDictionary; + AEdges: TList; + const AEntryPoint: string; + const AInterruptBefore: TArray; + const AContext: TNodeContext; + ACheckpointer: ICheckpointer; + AMaxIterations: Integer +); +var + Pair: TPair; + Edge: TEdge; +begin + inherited Create; + FNodes := TDictionary.Create; + if ANodes <> nil then + for Pair in ANodes do + FNodes.Add(Pair.Key, Pair.Value); + + FEdges := TList.Create; + if AEdges <> nil then + for Edge in AEdges do + FEdges.Add(Edge); + + FEntryPoint := AEntryPoint; + FInterruptBefore := Copy(AInterruptBefore); + FContext := AContext; + FCheckpointer := ACheckpointer; + if AMaxIterations <= 0 then + FMaxIterations := 15 + else + FMaxIterations := AMaxIterations; +end; + +destructor TCompiledAgent.Destroy; +begin + FHeldState.Free; + FEdges.Free; + FNodes.Free; + inherited; +end; + +function TCompiledAgent.GenerateThreadId: string; +begin + Result := TGUID.NewGuid.ToString.Replace('{', '').Replace('}', '').ToLower; +end; + +procedure TCompiledAgent.CheckpointSave(const AThreadId: string; AState: TAgentState); +begin + if (FCheckpointer = nil) or (AState = nil) then + Exit; + FCheckpointer.Save(AThreadId, AState.ToJson); +end; + +function TCompiledAgent.CheckpointLoad(const AThreadId: string): TAgentState; +begin + if FCheckpointer = nil then + raise EGraphError.Create('Checkpointer não configurado'); + Result := TAgentState.FromJson(FCheckpointer.Load(AThreadId)); +end; + +function TCompiledAgent.ShouldInterrupt(const ANodeName: string): Boolean; +var + S: string; + Node: TGraphNode; +begin + for S in FInterruptBefore do + if S = ANodeName then + Exit(True); + if FNodes.TryGetValue(ANodeName, Node) and Node.RequiresApproval then + Exit(True); + Result := False; +end; + +function TCompiledAgent.DescribePending(AState: TAgentState; const ANode: string): string; +begin + if (AState <> nil) and AState.HasPendingCalls then + Result := Format('Executar tool "%s" no nó %s', [AState.PendingCalls[0].Name, ANode]) + else + Result := 'Aguardando aprovação para executar o nó ' + ANode; +end; + +function TCompiledAgent.ExecuteNode( + const ANodeName: string; + const AState: TAgentState +): TAgentState; +var + Node: TGraphNode; +begin + if not FNodes.TryGetValue(ANodeName, Node) then + raise ENodeNotFound.CreateFmt('Nó não encontrado: %s', [ANodeName]); + if not Assigned(Node.Handler) then + raise EGraphExecutionError.CreateFmt('Handler ausente no nó "%s"', [ANodeName]); + Result := Node.Handler(AState, FContext); + if Result = nil then + raise EGraphExecutionError.CreateFmt('Nó "%s" retornou estado nulo', [ANodeName]); +end; + +function TCompiledAgent.ResolveNextNode( + const ACurrentNode: string; + const AState: TAgentState +): string; +var + Edge: TEdge; +begin + for Edge in FEdges do + begin + if Edge.SourceNode <> ACurrentNode then + Continue; + if Edge.Kind = ekFixed then + Exit(Edge.TargetNode); + if not Assigned(Edge.Condition) then + raise EGraphExecutionError.CreateFmt( + 'Edge condicional sem condição a partir de "%s"', [ACurrentNode]); + Result := Edge.Condition(AState); + if Result = '' then + Result := GRAPH_END; + Exit; + end; + Result := GRAPH_END; +end; + +function TCompiledAgent.ExecuteLoop( + AState: TAgentState; + ASkipFirstInterrupt: Boolean +): TGraphRunResult; +var + State: TAgentState; + NewState: TAgentState; + CurrentNode, NextNode: string; + I: Integer; +begin + State := AState; + Result := Default(TGraphRunResult); + if State <> nil then + Result.ThreadId := State.ThreadId; + + try + for I := 1 to FMaxIterations do + begin + if Assigned(FContext.Observer) then + FContext.Observer.OnIterationStart(I); + + CurrentNode := State.CurrentNode; + Result.Iterations := State.Iteration; + + if (CurrentNode = GRAPH_END) or (CurrentNode = '') then + begin + Result.Status := grsFinished; + Result.FinalAnswer := State.FinalAnswer; + if Assigned(FContext.Observer) then + FContext.Observer.OnFinished(Result.FinalAnswer, Result.Iterations); + Exit; + end; + + if (not ASkipFirstInterrupt) and ShouldInterrupt(CurrentNode) then + begin + CheckpointSave(State.ThreadId, State); + Result.Status := grsWaitingApproval; + Result.ThreadId := State.ThreadId; + Result.PendingNode := CurrentNode; + Result.PendingAction := DescribePending(State, CurrentNode); + Exit; + end; + ASkipFirstInterrupt := False; + + try + NewState := ExecuteNode(CurrentNode, State); + except + on E: Exception do + begin + Result.Status := grsError; + Result.ErrorMsg := E.Message; + Exit; + end; + end; + ReplaceState(State, NewState); + CheckpointSave(State.ThreadId, State); + + if State.IsDone then + begin + Result.Status := grsFinished; + Result.FinalAnswer := State.FinalAnswer; + Result.Iterations := State.Iteration; + if Assigned(FContext.Observer) then + FContext.Observer.OnFinished(Result.FinalAnswer, Result.Iterations); + Exit; + end; + + NextNode := ResolveNextNode(CurrentNode, State); + NewState := State.WithCurrentNode(NextNode); + ReplaceState(State, NewState); + NewState := State.NextIteration; + ReplaceState(State, NewState); + end; + + Result.Status := grsError; + Result.ErrorMsg := 'Limite de iterações atingido'; + Result.Iterations := FMaxIterations; + finally + State.Free; + end; +end; + +function TCompiledAgent.Run( + const AInput: string; + const AThreadId: string +): TGraphRunResult; +var + ThreadId: string; + State: TAgentState; +begin + if AThreadId = '' then + ThreadId := GenerateThreadId + else + ThreadId := AThreadId; + + if (FCheckpointer <> nil) and FCheckpointer.Exists(ThreadId) then + begin + State := CheckpointLoad(ThreadId); + // Só preserva o estado tal como está quando ele estiver genuinamente + // pausado num nó de aprovação (aguardando Resume). Em qualquer outro + // caso — concluído, em GRAPH_END, ou "preso" por um erro de execução + // anterior — trata como um novo turno, sob risco de descartar + // silenciosamente o AInput do usuário. + if State.IsDone or (State.CurrentNode = GRAPH_END) or (State.CurrentNode = '') + or not ShouldInterrupt(State.CurrentNode) then + begin + ReplaceState(State, State.RestartAt(FEntryPoint)); + ReplaceState(State, State.WithMessage(TLLMMessage.User(AInput))); + end; + end + else + begin + State := TAgentState.Create(ThreadId); + if FContext.Config.SystemPrompt <> '' then + ReplaceState(State, State.WithMessage(TLLMMessage.System(FContext.Config.SystemPrompt))); + ReplaceState(State, State.WithMessage(TLLMMessage.User(AInput))); + ReplaceState(State, State.WithCurrentNode(FEntryPoint)); + end; + + Result := ExecuteLoop(State, False); +end; + +function TCompiledAgent.Resume(const AThreadId: string): TGraphRunResult; +var + State: TAgentState; +begin + if (FCheckpointer = nil) or not FCheckpointer.Exists(AThreadId) then + raise EGraphError.CreateFmt('Nenhuma execução pausada para a thread %s', [AThreadId]); + State := CheckpointLoad(AThreadId); + Result := ExecuteLoop(State, True); +end; + +procedure TCompiledAgent.Cancel(const AThreadId: string); +begin + if FCheckpointer <> nil then + FCheckpointer.Delete(AThreadId); +end; + +function TCompiledAgent.GetState(const AThreadId: string): TObject; +begin + FreeAndNil(FHeldState); + if (FCheckpointer = nil) or not FCheckpointer.Exists(AThreadId) then + Exit(nil); + FHeldState := CheckpointLoad(AThreadId); + Result := FHeldState; +end; + +end. diff --git a/Sources/AI/Graph/Dext.AI.Graph.Contracts.pas b/Sources/AI/Graph/Dext.AI.Graph.Contracts.pas new file mode 100644 index 00000000..0fd50664 --- /dev/null +++ b/Sources/AI/Graph/Dext.AI.Graph.Contracts.pas @@ -0,0 +1,73 @@ +{***************************************************************************} +{ } +{ Dext Framework } +{ } +{ Dext.AI.Graph - Orquestração de agentes estilo LangGraph } +{ } +{***************************************************************************} +{ } +{ Description: } +{ Tipos, constantes e interfaces base do Dext.AI.Graph. } +{ Equivalente ao core do LangGraph (StateGraph / CompiledGraph). } +{ } +{***************************************************************************} +unit Dext.AI.Graph.Contracts; + +interface + +uses + System.SysUtils, + Dext.AI.Agent.Contracts; + +const + GRAPH_END = '__end__'; + GRAPH_START = '__start__'; + +type + TGraphRunStatus = ( + grsRunning, + grsFinished, + grsWaitingApproval, + grsError, + grsCancelled + ); + + TGraphRunResult = record + Status: TGraphRunStatus; + FinalAnswer: string; + ThreadId: string; + Iterations: Integer; + ErrorMsg: string; + PendingNode: string; + PendingAction: string; + end; + + ICompiledAgent = interface + ['{C3D4E5F6-A7B8-9012-CDEF-123456789012}'] + function Run( + const AInput: string; + const AThreadId: string = '' + ): TGraphRunResult; + + function Resume(const AThreadId: string): TGraphRunResult; + procedure Cancel(const AThreadId: string); + function GetState(const AThreadId: string): TObject; + end; + + ICheckpointer = interface + ['{D4E5F6A7-B8C9-0123-DEFA-234567890123}'] + procedure Save(const AThreadId: string; const AStateJson: string); + function Load(const AThreadId: string): string; + function Exists(const AThreadId: string): Boolean; + procedure Delete(const AThreadId: string); + end; + + EGraphError = class(Exception); + EGraphCompileError = class(EGraphError); + EGraphExecutionError = class(EGraphError); + ENodeNotFound = class(EGraphError); + ECycleDetected = class(EGraphError); + +implementation + +end. diff --git a/Sources/AI/Graph/Dext.AI.Graph.Edge.pas b/Sources/AI/Graph/Dext.AI.Graph.Edge.pas new file mode 100644 index 00000000..18e46f9b --- /dev/null +++ b/Sources/AI/Graph/Dext.AI.Graph.Edge.pas @@ -0,0 +1,91 @@ +{***************************************************************************} +{ } +{ Dext Framework } +{ } +{ Dext.AI.Graph - Orquestração de agentes estilo LangGraph } +{ } +{***************************************************************************} +{ } +{ Description: } +{ TEdge, TEdgeRoute e TEdgeCondition — edges do grafo. } +{ } +{***************************************************************************} +unit Dext.AI.Graph.Edge; + +interface + +uses + System.SysUtils, + Dext.AI.Graph.Contracts, + Dext.AI.Graph.State; + +type + TEdgeCondition = reference to function( + const AState: TAgentState + ): string; + + TEdgeRoute = record + TargetNode: string; + public + class function To_(const ANode: string): TEdgeRoute; static; + class function ToEnd: TEdgeRoute; static; + end; + + TEdgeKind = (ekFixed, ekConditional); + + TEdge = record + Kind: TEdgeKind; + SourceNode: string; + TargetNode: string; + Condition: TEdgeCondition; + Routes: TArray; + public + class function Fixed( + const ASource, ATarget: string + ): TEdge; static; + + class function Conditional( + const ASource: string; + ACondition: TEdgeCondition; + const ARoutes: TArray + ): TEdge; static; + end; + +implementation + +{ TEdgeRoute } + +class function TEdgeRoute.To_(const ANode: string): TEdgeRoute; +begin + Result.TargetNode := ANode; +end; + +class function TEdgeRoute.ToEnd: TEdgeRoute; +begin + Result.TargetNode := GRAPH_END; +end; + +{ TEdge } + +class function TEdge.Fixed(const ASource, ATarget: string): TEdge; +begin + Result := Default(TEdge); + Result.Kind := ekFixed; + Result.SourceNode := ASource; + Result.TargetNode := ATarget; +end; + +class function TEdge.Conditional( + const ASource: string; + ACondition: TEdgeCondition; + const ARoutes: TArray +): TEdge; +begin + Result := Default(TEdge); + Result.Kind := ekConditional; + Result.SourceNode := ASource; + Result.Condition := ACondition; + Result.Routes := ARoutes; +end; + +end. diff --git a/Sources/AI/Graph/Dext.AI.Graph.Graph.pas b/Sources/AI/Graph/Dext.AI.Graph.Graph.pas new file mode 100644 index 00000000..e6560ffb --- /dev/null +++ b/Sources/AI/Graph/Dext.AI.Graph.Graph.pas @@ -0,0 +1,331 @@ +{***************************************************************************} +{ } +{ Dext Framework } +{ } +{ Dext.AI.Graph - Orquestração de agentes estilo LangGraph } +{ } +{***************************************************************************} +{ } +{ Description: } +{ TAgentGraph — o StateGraph do Delphi. Define nós, edges e compila } +{ em ICompiledAgent. } +{ } +{***************************************************************************} +unit Dext.AI.Graph.Graph; + +interface + +uses + System.SysUtils, + System.Generics.Collections, + Dext.AI.Graph.Contracts, + Dext.AI.Graph.State, + Dext.AI.Graph.Edge, + Dext.AI.Agent.Contracts; + +type + TNodeContext = record + Provider: ILLMProvider; + Config: TAgentConfig; + Observer: IAgentObserver; + end; + + TNodeHandler = reference to function( + const AState: TAgentState; + const ACtx: TNodeContext + ): TAgentState; + + TGraphNode = record + Name: string; + Handler: TNodeHandler; + RequiresApproval: Boolean; + end; + + TAgentGraph = class + private + FNodes: TDictionary; + FEdges: TList; + FEntryPoint: string; + FInterruptBefore: TArray; + + procedure ValidateEntryPoint; + procedure ValidateNodesExist; + procedure ValidateReachability; + function FindNode(const AName: string): TGraphNode; + function CollectInterrupts: TArray; + function IsSpecialNode(const AName: string): Boolean; + public + constructor Create; + destructor Destroy; override; + + function AddNode( + const AName: string; + AHandler: TNodeHandler; + ARequiresApproval: Boolean = False + ): TAgentGraph; + + function SetEntryPoint(const ANode: string): TAgentGraph; + + function AddEdge( + const AFrom, ATo: string + ): TAgentGraph; + + function AddConditionalEdge( + const AFrom: string; + ACondition: TEdgeCondition; + const ARoutes: TArray + ): TAgentGraph; + + function Compile( + AProvider: ILLMProvider; + const AConfig: TAgentConfig; + AObserver: IAgentObserver = nil; + ACheckpointer: ICheckpointer = nil + ): ICompiledAgent; + + function InterruptBefore(const ANodes: TArray): TAgentGraph; + function RequireApproval(const ANode: string): TAgentGraph; + end; + +implementation + +uses + Dext.AI.Graph.Compiled, + Dext.AI.Graph.Checkpointer; + +{ TAgentGraph } + +constructor TAgentGraph.Create; +begin + inherited Create; + FNodes := TDictionary.Create; + FEdges := TList.Create; +end; + +destructor TAgentGraph.Destroy; +begin + FEdges.Free; + FNodes.Free; + inherited; +end; + +function TAgentGraph.IsSpecialNode(const AName: string): Boolean; +begin + Result := (AName = GRAPH_END) or (AName = GRAPH_START); +end; + +function TAgentGraph.FindNode(const AName: string): TGraphNode; +begin + if not FNodes.TryGetValue(AName, Result) then + raise ENodeNotFound.CreateFmt('Nó não encontrado: %s', [AName]); +end; + +function TAgentGraph.AddNode( + const AName: string; + AHandler: TNodeHandler; + ARequiresApproval: Boolean +): TAgentGraph; +var + Node: TGraphNode; +begin + if AName.Trim = '' then + raise EGraphCompileError.Create('Nome de nó vazio'); + if IsSpecialNode(AName) then + raise EGraphCompileError.CreateFmt('Nome reservado: %s', [AName]); + if not Assigned(AHandler) then + raise EGraphCompileError.CreateFmt('Handler ausente para o nó "%s"', [AName]); + if FNodes.ContainsKey(AName) then + raise EGraphCompileError.CreateFmt('Nó duplicado: %s', [AName]); + + Node.Name := AName; + Node.Handler := AHandler; + Node.RequiresApproval := ARequiresApproval; + FNodes.Add(AName, Node); + Result := Self; +end; + +function TAgentGraph.SetEntryPoint(const ANode: string): TAgentGraph; +begin + FEntryPoint := ANode; + Result := Self; +end; + +function TAgentGraph.AddEdge(const AFrom, ATo: string): TAgentGraph; +begin + FEdges.Add(TEdge.Fixed(AFrom, ATo)); + Result := Self; +end; + +function TAgentGraph.AddConditionalEdge( + const AFrom: string; + ACondition: TEdgeCondition; + const ARoutes: TArray +): TAgentGraph; +begin + if not Assigned(ACondition) then + raise EGraphCompileError.CreateFmt( + 'Condição ausente na edge condicional de "%s"', [AFrom]); + FEdges.Add(TEdge.Conditional(AFrom, ACondition, ARoutes)); + Result := Self; +end; + +function TAgentGraph.InterruptBefore(const ANodes: TArray): TAgentGraph; +begin + FInterruptBefore := Copy(ANodes); + Result := Self; +end; + +function TAgentGraph.RequireApproval(const ANode: string): TAgentGraph; +var + Node: TGraphNode; +begin + Node := FindNode(ANode); + Node.RequiresApproval := True; + FNodes.AddOrSetValue(ANode, Node); + Result := Self; +end; + +procedure TAgentGraph.ValidateEntryPoint; +begin + if FEntryPoint.Trim = '' then + raise EGraphCompileError.Create('Ponto de entrada não definido. Use SetEntryPoint.'); + if not FNodes.ContainsKey(FEntryPoint) then + raise ENodeNotFound.CreateFmt('Ponto de entrada inexistente: %s', [FEntryPoint]); +end; + +procedure TAgentGraph.ValidateNodesExist; +var + Edge: TEdge; + Route: TEdgeRoute; +begin + for Edge in FEdges do + begin + if not IsSpecialNode(Edge.SourceNode) and not FNodes.ContainsKey(Edge.SourceNode) then + raise ENodeNotFound.CreateFmt( + 'Edge referencia nó de origem inexistente: %s', [Edge.SourceNode]); + + if Edge.Kind = ekFixed then + begin + if not IsSpecialNode(Edge.TargetNode) and not FNodes.ContainsKey(Edge.TargetNode) then + raise ENodeNotFound.CreateFmt( + 'Edge referencia nó de destino inexistente: %s', [Edge.TargetNode]); + end + else + for Route in Edge.Routes do + if not IsSpecialNode(Route.TargetNode) and not FNodes.ContainsKey(Route.TargetNode) then + raise ENodeNotFound.CreateFmt( + 'Rota condicional referencia nó inexistente: %s', [Route.TargetNode]); + end; +end; + +procedure TAgentGraph.ValidateReachability; +var + Reachable: TDictionary; + Queue: TQueue; + Current: string; + Edge: TEdge; + Route: TEdgeRoute; + ReachedEnd: Boolean; + + procedure Visit(const ANode: string); + begin + if ANode = GRAPH_END then + begin + ReachedEnd := True; + Exit; + end; + if IsSpecialNode(ANode) then + Exit; + if Reachable.ContainsKey(ANode) then + Exit; + Reachable.Add(ANode, True); + Queue.Enqueue(ANode); + end; + +begin + if FNodes.Count = 0 then + raise EGraphCompileError.Create('Grafo sem nós'); + + Reachable := TDictionary.Create; + Queue := TQueue.Create; + try + ReachedEnd := False; + Visit(FEntryPoint); + + while Queue.Count > 0 do + begin + Current := Queue.Dequeue; + for Edge in FEdges do + begin + if Edge.SourceNode <> Current then + Continue; + if Edge.Kind = ekFixed then + Visit(Edge.TargetNode) + else + for Route in Edge.Routes do + Visit(Route.TargetNode); + end; + end; + + if not ReachedEnd then + raise ECycleDetected.Create( + 'Nenhum caminho do ponto de entrada até GRAPH_END'); + finally + Queue.Free; + Reachable.Free; + end; +end; + +function TAgentGraph.CollectInterrupts: TArray; +var + List: TList; + Pair: TPair; + S: string; +begin + List := TList.Create; + try + for S in FInterruptBefore do + if (S <> '') and (List.IndexOf(S) < 0) then + List.Add(S); + for Pair in FNodes do + if Pair.Value.RequiresApproval and (List.IndexOf(Pair.Key) < 0) then + List.Add(Pair.Key); + Result := List.ToArray; + finally + List.Free; + end; +end; + +function TAgentGraph.Compile( + AProvider: ILLMProvider; + const AConfig: TAgentConfig; + AObserver: IAgentObserver; + ACheckpointer: ICheckpointer +): ICompiledAgent; +var + Ctx: TNodeContext; + MaxIter: Integer; +begin + if AProvider = nil then + raise EGraphCompileError.Create('Provider LLM é obrigatório'); + + ValidateEntryPoint; + ValidateNodesExist; + ValidateReachability; + + Ctx.Provider := AProvider; + Ctx.Config := AConfig; + Ctx.Observer := AObserver; + + if ACheckpointer = nil then + ACheckpointer := TMemoryCheckpointer.Create; + + MaxIter := AConfig.MaxIterations; + if MaxIter <= 0 then + MaxIter := 15; + + Result := TCompiledAgent.Create( + FNodes, FEdges, FEntryPoint, CollectInterrupts, Ctx, ACheckpointer, MaxIter); +end; + +end. diff --git a/Sources/AI/Graph/Dext.AI.Graph.State.pas b/Sources/AI/Graph/Dext.AI.Graph.State.pas new file mode 100644 index 00000000..17f1fab0 --- /dev/null +++ b/Sources/AI/Graph/Dext.AI.Graph.State.pas @@ -0,0 +1,427 @@ +{***************************************************************************} +{ } +{ Dext Framework } +{ } +{ Dext.AI.Graph - Orquestração de agentes estilo LangGraph } +{ } +{***************************************************************************} +{ } +{ Description: } +{ TAgentState — estado imutável que flui pelo grafo. } +{ Cada With* retorna uma NOVA instância. Nenhum método muta Self. } +{ } +{***************************************************************************} +unit Dext.AI.Graph.State; + +interface + +uses + System.SysUtils, + System.JSON, + System.Generics.Collections, + Dext.AI.Agent.Contracts; + +type + TAgentState = class + private + FMessages: TArray; + FPendingCalls: TArray; + FCurrentNode: string; + FIteration: Integer; + FIsDone: Boolean; + FFinalAnswer: string; + FMetadata: TDictionary; + FThreadId: string; + + constructor CreateInternal( + const AMessages: TArray; + const APendingCalls: TArray; + const ACurrentNode: string; + AIteration: Integer; + AIsDone: Boolean; + const AFinalAnswer: string; + AMetadata: TDictionary; + const AThreadId: string + ); + function CloneMetadata: TDictionary; + function CloneMessages: TArray; + function ClonePendingCalls: TArray; + public + constructor Create(const AThreadId: string = ''); + destructor Destroy; override; + + function WithMessage(const AMsg: TLLMMessage): TAgentState; + function WithMessages(const AMsgs: TArray): TAgentState; + function WithPendingCalls(const ACalls: TArray): TAgentState; + function ClearPendingCalls: TAgentState; + function WithCurrentNode(const ANode: string): TAgentState; + function WithIteration(AIteration: Integer): TAgentState; + function NextIteration: TAgentState; + function AsDone(const AAnswer: string): TAgentState; + function WithMeta(const AKey, AValue: string): TAgentState; + function RestartAt(const ANode: string): TAgentState; + + function ToJson: string; + class function FromJson(const AJson: string): TAgentState; static; + + property Messages: TArray read FMessages; + property PendingCalls: TArray read FPendingCalls; + property CurrentNode: string read FCurrentNode; + property Iteration: Integer read FIteration; + property IsDone: Boolean read FIsDone; + property FinalAnswer: string read FFinalAnswer; + property ThreadId: string read FThreadId; + + function HasPendingCalls: Boolean; + function LastMessage: TLLMMessage; + function GetMeta(const AKey: string; const ADefault: string = ''): string; + end; + +implementation + +function RoleToName(ARole: TLLMRole): string; +begin + case ARole of + lrSystem: Result := 'system'; + lrUser: Result := 'user'; + lrAssistant: Result := 'assistant'; + lrToolResult: Result := 'tool'; + else + Result := 'user'; + end; +end; + +function NameToRole(const AName: string): TLLMRole; +var + LName: string; +begin + LName := AName.ToLower; + if LName = 'system' then + Result := lrSystem + else if LName = 'assistant' then + Result := lrAssistant + else if (LName = 'tool') or (LName = 'toolresult') then + Result := lrToolResult + else + Result := lrUser; +end; + +function MessageToJson(const AMsg: TLLMMessage): TJSONObject; +var + JCalls: TJSONArray; + JCall: TJSONObject; + TC: TLLMToolCall; +begin + Result := TJSONObject.Create; + Result.AddPair('role', RoleToName(AMsg.Role)); + Result.AddPair('content', AMsg.Content); + Result.AddPair('toolCallId', AMsg.ToolCallId); + JCalls := TJSONArray.Create; + for TC in AMsg.ToolCalls do + begin + JCall := TJSONObject.Create; + JCall.AddPair('id', TC.Id); + JCall.AddPair('name', TC.Name); + JCall.AddPair('argsJson', TC.ArgsJson); + JCalls.Add(JCall); + end; + Result.AddPair('toolCalls', JCalls); +end; + +function JsonToMessage(AObj: TJSONObject): TLLMMessage; +var + JCalls: TJSONArray; + JCallObj: TJSONObject; + TC: TLLMToolCall; + Calls: TArray; + I: Integer; +begin + Result := Default(TLLMMessage); + Result.Role := NameToRole(AObj.GetValue('role', 'user')); + Result.Content := AObj.GetValue('content', ''); + Result.ToolCallId := AObj.GetValue('toolCallId', ''); + JCalls := AObj.GetValue('toolCalls') as TJSONArray; + if JCalls = nil then + Exit; + SetLength(Calls, JCalls.Count); + for I := 0 to JCalls.Count - 1 do + begin + JCallObj := JCalls.Items[I] as TJSONObject; + TC := Default(TLLMToolCall); + TC.Id := JCallObj.GetValue('id', ''); + TC.Name := JCallObj.GetValue('name', ''); + TC.ArgsJson := JCallObj.GetValue('argsJson', ''); + Calls[I] := TC; + end; + Result.ToolCalls := Calls; +end; + +function ToolCallToJson(const ATC: TLLMToolCall): TJSONObject; +begin + Result := TJSONObject.Create; + Result.AddPair('id', ATC.Id); + Result.AddPair('name', ATC.Name); + Result.AddPair('argsJson', ATC.ArgsJson); +end; + +function JsonToToolCall(AObj: TJSONObject): TLLMToolCall; +begin + Result := Default(TLLMToolCall); + Result.Id := AObj.GetValue('id', ''); + Result.Name := AObj.GetValue('name', ''); + Result.ArgsJson := AObj.GetValue('argsJson', ''); +end; + +{ TAgentState } + +constructor TAgentState.Create(const AThreadId: string); +begin + inherited Create; + FThreadId := AThreadId; + FMetadata := TDictionary.Create; +end; + +constructor TAgentState.CreateInternal( + const AMessages: TArray; + const APendingCalls: TArray; + const ACurrentNode: string; + AIteration: Integer; + AIsDone: Boolean; + const AFinalAnswer: string; + AMetadata: TDictionary; + const AThreadId: string +); +begin + inherited Create; + FMessages := AMessages; + FPendingCalls := APendingCalls; + FCurrentNode := ACurrentNode; + FIteration := AIteration; + FIsDone := AIsDone; + FFinalAnswer := AFinalAnswer; + FThreadId := AThreadId; + if AMetadata <> nil then + FMetadata := AMetadata + else + FMetadata := TDictionary.Create; +end; + +destructor TAgentState.Destroy; +begin + FMetadata.Free; + inherited; +end; + +function TAgentState.CloneMetadata: TDictionary; +var + Pair: TPair; +begin + Result := TDictionary.Create; + if FMetadata = nil then + Exit; + for Pair in FMetadata do + Result.AddOrSetValue(Pair.Key, Pair.Value); +end; + +function TAgentState.CloneMessages: TArray; +begin + Result := Copy(FMessages); +end; + +function TAgentState.ClonePendingCalls: TArray; +begin + Result := Copy(FPendingCalls); +end; + +function TAgentState.WithMessage(const AMsg: TLLMMessage): TAgentState; +var + Msgs: TArray; +begin + Msgs := CloneMessages; + SetLength(Msgs, Length(Msgs) + 1); + Msgs[High(Msgs)] := AMsg; + Result := TAgentState.CreateInternal( + Msgs, ClonePendingCalls, FCurrentNode, FIteration, FIsDone, + FFinalAnswer, CloneMetadata, FThreadId); +end; + +function TAgentState.WithMessages(const AMsgs: TArray): TAgentState; +begin + Result := TAgentState.CreateInternal( + Copy(AMsgs), ClonePendingCalls, FCurrentNode, FIteration, FIsDone, + FFinalAnswer, CloneMetadata, FThreadId); +end; + +function TAgentState.WithPendingCalls(const ACalls: TArray): TAgentState; +begin + Result := TAgentState.CreateInternal( + CloneMessages, Copy(ACalls), FCurrentNode, FIteration, FIsDone, + FFinalAnswer, CloneMetadata, FThreadId); +end; + +function TAgentState.ClearPendingCalls: TAgentState; +begin + Result := TAgentState.CreateInternal( + CloneMessages, nil, FCurrentNode, FIteration, FIsDone, + FFinalAnswer, CloneMetadata, FThreadId); +end; + +function TAgentState.WithCurrentNode(const ANode: string): TAgentState; +begin + Result := TAgentState.CreateInternal( + CloneMessages, ClonePendingCalls, ANode, FIteration, FIsDone, + FFinalAnswer, CloneMetadata, FThreadId); +end; + +function TAgentState.WithIteration(AIteration: Integer): TAgentState; +begin + Result := TAgentState.CreateInternal( + CloneMessages, ClonePendingCalls, FCurrentNode, AIteration, FIsDone, + FFinalAnswer, CloneMetadata, FThreadId); +end; + +function TAgentState.NextIteration: TAgentState; +begin + Result := WithIteration(FIteration + 1); +end; + +function TAgentState.AsDone(const AAnswer: string): TAgentState; +begin + Result := TAgentState.CreateInternal( + CloneMessages, ClonePendingCalls, FCurrentNode, FIteration, True, + AAnswer, CloneMetadata, FThreadId); +end; + +function TAgentState.WithMeta(const AKey, AValue: string): TAgentState; +var + Meta: TDictionary; +begin + Meta := CloneMetadata; + Meta.AddOrSetValue(AKey, AValue); + Result := TAgentState.CreateInternal( + CloneMessages, ClonePendingCalls, FCurrentNode, FIteration, FIsDone, + FFinalAnswer, Meta, FThreadId); +end; + +function TAgentState.RestartAt(const ANode: string): TAgentState; +begin + Result := TAgentState.CreateInternal( + CloneMessages, nil, ANode, 0, False, '', CloneMetadata, FThreadId); +end; + +function TAgentState.HasPendingCalls: Boolean; +begin + Result := Length(FPendingCalls) > 0; +end; + +function TAgentState.LastMessage: TLLMMessage; +begin + if Length(FMessages) = 0 then + Result := Default(TLLMMessage) + else + Result := FMessages[High(FMessages)]; +end; + +function TAgentState.GetMeta(const AKey: string; const ADefault: string): string; +begin + if (FMetadata = nil) or not FMetadata.TryGetValue(AKey, Result) then + Result := ADefault; +end; + +function TAgentState.ToJson: string; +var + Root: TJSONObject; + JMsgs, JCalls: TJSONArray; + JMeta: TJSONObject; + Msg: TLLMMessage; + TC: TLLMToolCall; + Pair: TPair; +begin + Root := TJSONObject.Create; + try + Root.AddPair('threadId', FThreadId); + Root.AddPair('currentNode', FCurrentNode); + Root.AddPair('iteration', TJSONNumber.Create(FIteration)); + Root.AddPair('isDone', TJSONBool.Create(FIsDone)); + Root.AddPair('finalAnswer', FFinalAnswer); + + JMsgs := TJSONArray.Create; + for Msg in FMessages do + JMsgs.Add(MessageToJson(Msg)); + Root.AddPair('messages', JMsgs); + + JCalls := TJSONArray.Create; + for TC in FPendingCalls do + JCalls.Add(ToolCallToJson(TC)); + Root.AddPair('pendingCalls', JCalls); + + JMeta := TJSONObject.Create; + if FMetadata <> nil then + for Pair in FMetadata do + JMeta.AddPair(Pair.Key, Pair.Value); + Root.AddPair('metadata', JMeta); + + Result := Root.ToJSON; + finally + Root.Free; + end; +end; + +class function TAgentState.FromJson(const AJson: string): TAgentState; +var + Root: TJSONObject; + JMsgs, JCalls: TJSONArray; + JMeta: TJSONObject; + JVal: TJSONValue; + Msgs: TArray; + Calls: TArray; + Meta: TDictionary; + I: Integer; + Pair: TJSONPair; +begin + Root := TJSONObject.ParseJSONValue(AJson) as TJSONObject; + if Root = nil then + raise EArgumentException.Create('JSON de estado inválido'); + try + JMsgs := Root.GetValue('messages') as TJSONArray; + if JMsgs <> nil then + begin + SetLength(Msgs, JMsgs.Count); + for I := 0 to JMsgs.Count - 1 do + Msgs[I] := JsonToMessage(JMsgs.Items[I] as TJSONObject); + end; + + JCalls := Root.GetValue('pendingCalls') as TJSONArray; + if JCalls <> nil then + begin + SetLength(Calls, JCalls.Count); + for I := 0 to JCalls.Count - 1 do + Calls[I] := JsonToToolCall(JCalls.Items[I] as TJSONObject); + end; + + Meta := TDictionary.Create; + JMeta := Root.GetValue('metadata') as TJSONObject; + if JMeta <> nil then + for Pair in JMeta do + begin + JVal := Pair.JsonValue; + if JVal <> nil then + Meta.AddOrSetValue(Pair.JsonString.Value, JVal.Value); + end; + + Result := TAgentState.CreateInternal( + Msgs, + Calls, + Root.GetValue('currentNode', ''), + Root.GetValue('iteration', 0), + Root.GetValue('isDone', False), + Root.GetValue('finalAnswer', ''), + Meta, + Root.GetValue('threadId', '') + ); + finally + Root.Free; + end; +end; + +end. diff --git a/Sources/AI/Graph/Nodes/Dext.AI.Graph.Node.LLM.pas b/Sources/AI/Graph/Nodes/Dext.AI.Graph.Node.LLM.pas new file mode 100644 index 00000000..8e9c6c7f --- /dev/null +++ b/Sources/AI/Graph/Nodes/Dext.AI.Graph.Node.LLM.pas @@ -0,0 +1,123 @@ +{***************************************************************************} +{ } +{ Dext Framework } +{ } +{ Dext.AI.Graph - Orquestração de agentes estilo LangGraph } +{ } +{***************************************************************************} +{ } +{ Description: } +{ TLLMNode — nó padrão que chama o provider LLM (call_model). } +{ } +{***************************************************************************} +unit Dext.AI.Graph.Node.LLM; + +interface + +uses + Dext.AI.Graph.Contracts, + Dext.AI.Graph.State, + Dext.AI.Graph.Graph, + Dext.AI.Agent.Contracts, + System.SysUtils; + +type + TLLMNode = class + private + FToolSchemas: TArray; + public + constructor Create(const AToolSchemas: TArray); + function GetAsHandler: TNodeHandler; + function Execute( + const AState: TAgentState; + const ACtx: TNodeContext + ): TAgentState; + + property AsHandler: TNodeHandler read GetAsHandler; + end; + +function DefaultShouldContinue(const AState: TAgentState): string; + +implementation + +function StopReasonToString(AReason: TLLMStopReason): string; +begin + case AReason of + srEndTurn: Result := 'srEndTurn'; + srToolUse: Result := 'srToolUse'; + srMaxTokens: Result := 'srMaxTokens'; + srError: Result := 'srError'; + else + Result := 'unknown'; + end; +end; + +{ TLLMNode } + +constructor TLLMNode.Create(const AToolSchemas: TArray); +begin + inherited Create; + FToolSchemas := Copy(AToolSchemas); +end; + +function TLLMNode.GetAsHandler: TNodeHandler; +begin + Result := + function(const AState: TAgentState; const ACtx: TNodeContext): TAgentState + begin + Result := Self.Execute(AState, ACtx); + end; +end; + +function TLLMNode.Execute( + const AState: TAgentState; + const ACtx: TNodeContext +): TAgentState; +var + Response: TLLMResponse; + AssistantMsg: TLLMMessage; + Intermediate: TAgentState; +begin + if ACtx.Provider = nil then + raise EGraphExecutionError.Create('Provider LLM ausente no contexto do nó'); + + Response := ACtx.Provider.Complete(AState.Messages, FToolSchemas); + + if Assigned(ACtx.Observer) then + ACtx.Observer.OnLLMResponse(Response.Content, Response.StopReason); + + case Response.StopReason of + srEndTurn: + begin + AssistantMsg := TLLMMessage.Assistant(Response.Content); + Intermediate := AState.WithMessage(AssistantMsg); + try + Result := Intermediate.AsDone(Response.Content); + finally + Intermediate.Free; + end; + end; + srToolUse: + begin + AssistantMsg := TLLMMessage.Assistant(Response.Content, Response.ToolCalls); + Intermediate := AState.WithMessage(AssistantMsg); + try + Result := Intermediate.WithPendingCalls(Response.ToolCalls); + finally + Intermediate.Free; + end; + end; + else + Result := AState.AsDone('[Error: ' + StopReasonToString(Response.StopReason) + ']'); + end; +end; + +function DefaultShouldContinue(const AState: TAgentState): string; +begin + if AState.HasPendingCalls then + Result := 'execute_tools' + else + Result := GRAPH_END; +end; + +end. diff --git a/Sources/AI/Graph/Nodes/Dext.AI.Graph.Node.Tools.pas b/Sources/AI/Graph/Nodes/Dext.AI.Graph.Node.Tools.pas new file mode 100644 index 00000000..a395642f --- /dev/null +++ b/Sources/AI/Graph/Nodes/Dext.AI.Graph.Node.Tools.pas @@ -0,0 +1,278 @@ +{***************************************************************************} +{ } +{ Dext Framework } +{ } +{ Dext.AI.Graph - Orquestração de agentes estilo LangGraph } +{ } +{***************************************************************************} +{ } +{ Description: } +{ TToolsNode — nó padrão que executa as tool calls pendentes (ToolNode). } +{ } +{***************************************************************************} +unit Dext.AI.Graph.Node.Tools; + +interface + +uses + Dext.AI.Graph.State, + Dext.AI.Graph.Graph, + Dext.AI.Agent.Contracts, + Dext.AI.MCP.Tools, + Dext.AI.MCP.Attributes, + Dext.AI.MCP.Types, + Dext.AI.MCP.Protocol, + System.Rtti, + System.JSON, + System.SysUtils, + System.Generics.Collections; + +type + TToolsNode = class + private + FProviders: TObjectList; + + function ExecuteSingleTool( + const AToolName, AArgsJson: string + ): string; + + function BuildInputSchema(AMethod: TRttiMethod): string; + function BuildToolSchemas: TArray; + function ToolResultToText(const AResult: TMCPToolResult): string; + public + constructor Create; + destructor Destroy; override; + + procedure RegisterProvider(AProvider: TMCPToolProvider); + function GetToolSchemas: TArray; + function GetAsHandler: TNodeHandler; + function Execute( + const AState: TAgentState; + const ACtx: TNodeContext + ): TAgentState; + + property AsHandler: TNodeHandler read GetAsHandler; + end; + +implementation + +uses + System.Classes; + +{ TToolsNode } + +constructor TToolsNode.Create; +begin + inherited Create; + FProviders := TObjectList.Create(True); +end; + +destructor TToolsNode.Destroy; +begin + FProviders.Free; + inherited; +end; + +procedure TToolsNode.RegisterProvider(AProvider: TMCPToolProvider); +begin + if AProvider <> nil then + FProviders.Add(AProvider); +end; + +function TToolsNode.GetAsHandler: TNodeHandler; +begin + Result := + function(const AState: TAgentState; const ACtx: TNodeContext): TAgentState + begin + Result := Self.Execute(AState, ACtx); + end; +end; + +function TToolsNode.GetToolSchemas: TArray; +begin + Result := BuildToolSchemas; +end; + +function TToolsNode.BuildInputSchema(AMethod: TRttiMethod): string; +var + JSchema, JProps, JParam: TJSONObject; + JRequired: TJSONArray; + Attr: TCustomAttribute; + ParamAttr: MCPParamAttribute; +begin + JProps := TJSONObject.Create; + JRequired := TJSONArray.Create; + + for Attr in AMethod.GetAttributes do + if Attr is MCPParamAttribute then + begin + ParamAttr := MCPParamAttribute(Attr); + + JParam := TJSONObject.Create; + JParam.AddPair('description', ParamAttr.Description); + case ParamAttr.ParamType of + ptString: JParam.AddPair('type', 'string'); + ptInteger: JParam.AddPair('type', 'integer'); + ptNumber: JParam.AddPair('type', 'number'); + ptBoolean: JParam.AddPair('type', 'boolean'); + end; + JProps.AddPair(ParamAttr.Name, JParam); + + if ParamAttr.Required then + JRequired.Add(ParamAttr.Name); + end; + + JSchema := TJSONObject.Create; + try + JSchema.AddPair('type', 'object'); + JSchema.AddPair('properties', JProps); + if JRequired.Count > 0 then + JSchema.AddPair('required', JRequired) + else + JRequired.Free; + Result := JSchema.ToJSON; + finally + JSchema.Free; + end; +end; + +function TToolsNode.BuildToolSchemas: TArray; +var + Ctx: TRttiContext; + Provider: TMCPToolProvider; + Method: TRttiMethod; + ToolAttr: MCPToolAttribute; + Schemas: TList; + Schema: TToolSchema; +begin + Ctx := TRttiContext.Create; + Schemas := TList.Create; + try + for Provider in FProviders do + for Method in Ctx.GetType(Provider.ClassType).GetMethods do + begin + ToolAttr := Method.GetAttribute; + if ToolAttr = nil then + Continue; + + Schema := Default(TToolSchema); + Schema.Name := ToolAttr.Name; + Schema.Description := ToolAttr.Description; + Schema.InputSchema := BuildInputSchema(Method); + Schemas.Add(Schema); + end; + + Result := Schemas.ToArray; + finally + Schemas.Free; + Ctx.Free; + end; +end; + +function TToolsNode.ToolResultToText(const AResult: TMCPToolResult): string; +var + Item: TMCPContent; + Parts: TStringBuilder; +begin + Parts := TStringBuilder.Create; + try + for Item in AResult.Content do + if Item.ContentType = mctText then + begin + if Parts.Length > 0 then + Parts.Append(sLineBreak); + Parts.Append(Item.TextValue); + end; + + Result := Parts.ToString; + if AResult.IsError then + Result := '[Error] ' + Result; + finally + Parts.Free; + end; +end; + +function TToolsNode.ExecuteSingleTool( + const AToolName, AArgsJson: string +): string; +var + Ctx: TRttiContext; + Provider: TMCPToolProvider; + RttiType: TRttiType; + Method: TRttiMethod; + ToolAttr: MCPToolAttribute; + JArgs: TJSONObject; + InvokeResult: TValue; +begin + Ctx := TRttiContext.Create; + try + JArgs := TJSONObject.ParseJSONValue(AArgsJson) as TJSONObject; + if JArgs = nil then + JArgs := TJSONObject.Create; + try + for Provider in FProviders do + begin + RttiType := Ctx.GetType(Provider.ClassType); + for Method in RttiType.GetMethods do + begin + ToolAttr := Method.GetAttribute; + if (ToolAttr = nil) or (ToolAttr.Name <> AToolName) then + Continue; + + try + Provider.BeforeCall(AToolName, JArgs); + InvokeResult := Method.Invoke(Provider, [TValue.From(JArgs)]); + Provider.AfterCall(AToolName); + Exit(ToolResultToText(InvokeResult.AsType)); + except + on E: Exception do + Exit('[Error] ' + E.Message); + end; + end; + end; + + Result := '[Error: Tool not found: ' + AToolName + ']'; + finally + JArgs.Free; + end; + finally + Ctx.Free; + end; +end; + +function TToolsNode.Execute( + const AState: TAgentState; + const ACtx: TNodeContext +): TAgentState; +var + NewState: TAgentState; + TC: TLLMToolCall; + ToolResultText: string; + Old: TAgentState; +begin + NewState := AState; + for TC in AState.PendingCalls do + begin + if Assigned(ACtx.Observer) then + ACtx.Observer.OnToolCall(TC.Name, TC.ArgsJson); + + ToolResultText := ExecuteSingleTool(TC.Name, TC.ArgsJson); + + if Assigned(ACtx.Observer) then + ACtx.Observer.OnToolResult(TC.Name, ToolResultText); + + Old := NewState; + NewState := NewState.WithMessage(TLLMMessage.ToolResult(TC.Id, ToolResultText)); + if (Old <> AState) and (Old <> NewState) then + Old.Free; + end; + + Old := NewState; + NewState := NewState.ClearPendingCalls; + if (Old <> AState) and (Old <> NewState) then + Old.Free; + + Result := NewState; +end; + +end. From cfd9af0a818647893299d53265123d6787aad884 Mon Sep 17 00:00:00 2001 From: alepmedeiros Date: Mon, 31 Aug 2026 08:22:40 -0300 Subject: [PATCH 2/4] Add subgraph-as-node support to Dext.AI.Graph (ICompiledAgent.AsNode) Lets a compiled graph be embedded as a single node in a parent TAgentGraph (TAgentGraph.AddNode('x', SubAgent.AsNode)), enabling composition of reusable sub-agents (e.g. a Fiscal sub-graph inside a larger ERP graph) without flattening every sub-agent's nodes into the parent graph. - TNodeContext/TNodeHandler moved from Dext.AI.Graph.Graph into Dext.AI.Graph.Contracts, since ICompiledAgent.AsNode needs to return TNodeHandler and Contracts can't depend on Graph (would be circular). - TCompiledAgent.RunAsSubgraph runs the subgraph from its own entry point through its own GRAPH_END/IsDone directly against the shared TAgentState (no schema translation needed - state is not per-graph typed), then clears IsDone before returning so the parent's own edges decide what happens next. - AsNode raises EGraphCompileError up front for graphs with RequireApproval/InterruptBefore - nested human-in-the-loop isn't supported yet, so this fails loudly instead of silently skipping the approval step. - Fixed TAgentGraph.ValidateReachability to match ResolveNextNode's runtime fallback: a node with no outgoing edge implicitly reaches GRAPH_END. Without this, valid single-node terminal graphs (the minimal shape needed for a subgraph) were rejected at compile time with a false ECycleDetected. Verified with a standalone harness (fake ILLMProvider, no API key needed): state propagates from subgraph to parent, the parent continues past the subgraph node via its own edges, the interrupt guard rejects RequireApproval subgraphs, and an exception inside a subgraph surfaces as grsError in the parent without corrupting state. Co-Authored-By: Claude Sonnet 5 --- Sources/AI/Graph/Dext.AI.Graph.Compiled.pas | 80 +++++++++++++++++++ Sources/AI/Graph/Dext.AI.Graph.Contracts.pas | 25 +++++- Sources/AI/Graph/Dext.AI.Graph.Graph.pas | 22 ++--- Sources/AI/Graph/Dext.AI.Graph.State.pas | 8 ++ .../Graph/Nodes/Dext.AI.Graph.Node.Tools.pas | 1 + 5 files changed, 125 insertions(+), 11 deletions(-) diff --git a/Sources/AI/Graph/Dext.AI.Graph.Compiled.pas b/Sources/AI/Graph/Dext.AI.Graph.Compiled.pas index 41607fbc..2fde40e2 100644 --- a/Sources/AI/Graph/Dext.AI.Graph.Compiled.pas +++ b/Sources/AI/Graph/Dext.AI.Graph.Compiled.pas @@ -53,6 +53,12 @@ TCompiledAgent = class(TInterfacedObject, ICompiledAgent) function CheckpointLoad(const AThreadId: string): TAgentState; function ExecuteLoop(AState: TAgentState; ASkipFirstInterrupt: Boolean): TGraphRunResult; function DescribePending(AState: TAgentState; const ANode: string): string; + + // Executa este grafo compilado como um único nó de um grafo pai + // (subgraph-as-node): roda do próprio EntryPoint até o próprio + // GRAPH_END/IsDone e devolve o TAgentState resultante — sem produzir + // TGraphRunResult, sem checkpointing próprio (o pai é quem persiste). + function RunAsSubgraph(const AState: TAgentState): TAgentState; public constructor Create( ANodes: TDictionary; @@ -73,6 +79,7 @@ TCompiledAgent = class(TInterfacedObject, ICompiledAgent) function Resume(const AThreadId: string): TGraphRunResult; procedure Cancel(const AThreadId: string); function GetState(const AThreadId: string): TObject; + function AsNode: TNodeHandler; end; implementation @@ -356,4 +363,77 @@ function TCompiledAgent.GetState(const AThreadId: string): TObject; Result := FHeldState; end; +function TCompiledAgent.RunAsSubgraph(const AState: TAgentState): TAgentState; +var + State, NewState: TAgentState; + CurrentNode, NextNode: string; + I: Integer; + Finished: Boolean; +begin + // Entra pelo próprio EntryPoint do subgrafo, preservando mensagens/ + // metadata/threadId do estado do pai — TAgentState é o mesmo tipo + // concreto nos dois grafos, então não há tradução de schema a fazer. + State := AState.RestartAt(FEntryPoint); + Finished := False; + try + for I := 1 to FMaxIterations do + begin + if Assigned(FContext.Observer) then + FContext.Observer.OnIterationStart(I); + + CurrentNode := State.CurrentNode; + if (CurrentNode = GRAPH_END) or (CurrentNode = '') then + begin + Finished := True; + Break; + end; + + NewState := ExecuteNode(CurrentNode, State); + ReplaceState(State, NewState); + + if State.IsDone then + begin + Finished := True; + Break; + end; + + NextNode := ResolveNextNode(CurrentNode, State); + NewState := State.WithCurrentNode(NextNode); + ReplaceState(State, NewState); + NewState := State.NextIteration; + ReplaceState(State, NewState); + end; + + if not Finished then + raise EGraphExecutionError.CreateFmt( + 'Subgrafo excedeu o limite de %d iterações sem atingir GRAPH_END', [FMaxIterations]); + + // IsDone aqui é um sinal interno do subgrafo, não do grafo pai — o + // pai decide o que acontece depois via suas próprias edges a partir + // do nó que envolve este subgrafo. + NewState := State.ClearDone; + ReplaceState(State, NewState); + + Result := State; + State := nil; + finally + State.Free; + end; +end; + +function TCompiledAgent.AsNode: TNodeHandler; +begin + if Length(FInterruptBefore) > 0 then + raise EGraphCompileError.Create( + 'Grafos com RequireApproval/InterruptBefore não podem ser usados como ' + + 'subgrafo (AsNode) — aprovação humana aninhada não é suportada. ' + + 'Configure RequireApproval no nó do grafo pai que invoca este subgrafo.'); + + Result := + function(const AState: TAgentState; const ACtx: TNodeContext): TAgentState + begin + Result := Self.RunAsSubgraph(AState); + end; +end; + end. diff --git a/Sources/AI/Graph/Dext.AI.Graph.Contracts.pas b/Sources/AI/Graph/Dext.AI.Graph.Contracts.pas index 0fd50664..4514747c 100644 --- a/Sources/AI/Graph/Dext.AI.Graph.Contracts.pas +++ b/Sources/AI/Graph/Dext.AI.Graph.Contracts.pas @@ -17,7 +17,8 @@ interface uses System.SysUtils, - Dext.AI.Agent.Contracts; + Dext.AI.Agent.Contracts, + Dext.AI.Graph.State; const GRAPH_END = '__end__'; @@ -42,6 +43,21 @@ TGraphRunResult = record PendingAction: string; end; + // Contexto de execução compartilhado, injetado em cada nó do grafo. + TNodeContext = record + Provider: ILLMProvider; + Config: TAgentConfig; + Observer: IAgentObserver; + end; + + // Handler de nó: recebe o estado atual e devolve o novo estado. + // Declarado aqui (não em Dext.AI.Graph.Graph) porque ICompiledAgent.AsNode + // precisa expor esse tipo, e Contracts não pode depender de Graph. + TNodeHandler = reference to function( + const AState: TAgentState; + const ACtx: TNodeContext + ): TAgentState; + ICompiledAgent = interface ['{C3D4E5F6-A7B8-9012-CDEF-123456789012}'] function Run( @@ -52,6 +68,13 @@ TGraphRunResult = record function Resume(const AThreadId: string): TGraphRunResult; procedure Cancel(const AThreadId: string); function GetState(const AThreadId: string): TObject; + + // Adapta este grafo compilado para ser usado como um nó comum de um + // grafo pai (subgraph-as-node). O estado é passado direto — sem + // tradução — pois TAgentState já é o mesmo tipo em ambos os grafos. + // Grafos com RequireApproval/InterruptBefore levantam EGraphCompileError + // aqui: aprovação humana aninhada não é suportada (v1). + function AsNode: TNodeHandler; end; ICheckpointer = interface diff --git a/Sources/AI/Graph/Dext.AI.Graph.Graph.pas b/Sources/AI/Graph/Dext.AI.Graph.Graph.pas index e6560ffb..9cbc133e 100644 --- a/Sources/AI/Graph/Dext.AI.Graph.Graph.pas +++ b/Sources/AI/Graph/Dext.AI.Graph.Graph.pas @@ -24,16 +24,9 @@ interface Dext.AI.Agent.Contracts; type - TNodeContext = record - Provider: ILLMProvider; - Config: TAgentConfig; - Observer: IAgentObserver; - end; - - TNodeHandler = reference to function( - const AState: TAgentState; - const ACtx: TNodeContext - ): TAgentState; + // TNodeContext e TNodeHandler agora vivem em Dext.AI.Graph.Contracts + // (ICompiledAgent.AsNode precisa do tipo, e Contracts não pode depender + // desta unit). Ficam visíveis aqui via o uses acima. TGraphNode = record Name: string; @@ -226,6 +219,7 @@ procedure TAgentGraph.ValidateReachability; Edge: TEdge; Route: TEdgeRoute; ReachedEnd: Boolean; + HasOutgoing: Boolean; procedure Visit(const ANode: string); begin @@ -255,16 +249,24 @@ procedure TAgentGraph.ValidateReachability; while Queue.Count > 0 do begin Current := Queue.Dequeue; + HasOutgoing := False; for Edge in FEdges do begin if Edge.SourceNode <> Current then Continue; + HasOutgoing := True; if Edge.Kind = ekFixed then Visit(Edge.TargetNode) else for Route in Edge.Routes do Visit(Route.TargetNode); end; + // Um nó sem nenhuma edge de saída termina implicitamente em + // GRAPH_END em runtime (ResolveNextNode faz esse fallback) — a + // validação precisa refletir o mesmo comportamento, senão rejeita + // grafos mínimos válidos de um único nó terminal. + if not HasOutgoing then + ReachedEnd := True; end; if not ReachedEnd then diff --git a/Sources/AI/Graph/Dext.AI.Graph.State.pas b/Sources/AI/Graph/Dext.AI.Graph.State.pas index 17f1fab0..5c474629 100644 --- a/Sources/AI/Graph/Dext.AI.Graph.State.pas +++ b/Sources/AI/Graph/Dext.AI.Graph.State.pas @@ -60,6 +60,7 @@ TAgentState = class function AsDone(const AAnswer: string): TAgentState; function WithMeta(const AKey, AValue: string): TAgentState; function RestartAt(const ANode: string): TAgentState; + function ClearDone: TAgentState; function ToJson: string; class function FromJson(const AJson: string): TAgentState; static; @@ -309,6 +310,13 @@ function TAgentState.RestartAt(const ANode: string): TAgentState; CloneMessages, nil, ANode, 0, False, '', CloneMetadata, FThreadId); end; +function TAgentState.ClearDone: TAgentState; +begin + Result := TAgentState.CreateInternal( + CloneMessages, ClonePendingCalls, FCurrentNode, FIteration, False, + FFinalAnswer, CloneMetadata, FThreadId); +end; + function TAgentState.HasPendingCalls: Boolean; begin Result := Length(FPendingCalls) > 0; diff --git a/Sources/AI/Graph/Nodes/Dext.AI.Graph.Node.Tools.pas b/Sources/AI/Graph/Nodes/Dext.AI.Graph.Node.Tools.pas index a395642f..92ebd347 100644 --- a/Sources/AI/Graph/Nodes/Dext.AI.Graph.Node.Tools.pas +++ b/Sources/AI/Graph/Nodes/Dext.AI.Graph.Node.Tools.pas @@ -15,6 +15,7 @@ interface uses + Dext.AI.Graph.Contracts, Dext.AI.Graph.State, Dext.AI.Graph.Graph, Dext.AI.Agent.Contracts, From eccc43ad281da138fe221a578f7bc43e46101a20 Mon Sep 17 00:00:00 2001 From: alepmedeiros Date: Mon, 31 Aug 2026 08:32:12 -0300 Subject: [PATCH 3/4] Wire AsNode subgraph and TFileCheckpointer into GraphDemo GraphDemo previously only exercised the sequential/conditional-edge and RequireApproval parts of Dext.AI.Graph, leaving two shipped primitives completely unused anywhere in the repo: ICompiledAgent.AsNode and TFileCheckpointer. Since the example is the reference for how to use the framework, wire both in: - Compile a small independent PolishGraph (single node, no tools) and embed it as the 'polish_agent' node of the main graph via PolishAgent.AsNode, routed to from call_llm's conditional edge instead of going straight to GRAPH_END. Its rewritten answer becomes the run's FinalAnswer by design (AsNode preserves FinalAnswer while clearing IsDone, so the parent's own edges decide what happens next). - Switch the main checkpointer from TMemoryCheckpointer to TFileCheckpointer so the thread survives across process restarts, and print its path on startup. - Add a ":estado" command to the input loop that calls Agent.GetState to inspect the persisted thread without running it. Comment on the subgraph documents the one hard constraint: a subgraph node can't itself use RequireApproval/InterruptBefore (AsNode raises EGraphCompileError) - approval has to sit on the parent node that wraps the subgraph call. Compiles clean on Win32/Win64. Not yet live-tested against a real OPENAI_API_KEY in this session - the polish_agent routing and FinalAnswer override should be verified end-to-end before relying on it. Co-Authored-By: Claude Sonnet 5 --- Examples/AI/GraphDemo/GraphDemo.dpr | 108 +++++++++++++++++++++++----- 1 file changed, 90 insertions(+), 18 deletions(-) diff --git a/Examples/AI/GraphDemo/GraphDemo.dpr b/Examples/AI/GraphDemo/GraphDemo.dpr index 8b239b5e..9781b39c 100644 --- a/Examples/AI/GraphDemo/GraphDemo.dpr +++ b/Examples/AI/GraphDemo/GraphDemo.dpr @@ -141,18 +141,46 @@ begin end; end; +// Condição de roteamento do nó 'call_llm'. Igual ao DefaultShouldContinue, +// mas manda o fluxo passar pelo subgrafo 'polish_agent' antes do GRAPH_END, +// em vez de encerrar direto — é assim que se pluga um subgrafo no meio do +// roteamento condicional de um grafo existente. +function ShouldContinueOrPolish(const AState: TAgentState): string; +begin + if AState.HasPendingCalls then + Result := 'execute_tools' + else + Result := 'polish_agent'; +end; + var - Config: TAgentConfig; - Provider: ILLMProvider; - Observer: IAgentObserver; - ToolsNode: TToolsNode; - LLMNode: TLLMNode; - Graph: TAgentGraph; - Agent: ICompiledAgent; - Checkpointer: ICheckpointer; - Input, ThreadId: string; - RunResult: TGraphRunResult; - Confirm: string; + Config: TAgentConfig; + Provider: ILLMProvider; + Observer: IAgentObserver; + ToolsNode: TToolsNode; + LLMNode: TLLMNode; + Graph: TAgentGraph; + Agent: ICompiledAgent; + Checkpointer: ICheckpointer; + Input, ThreadId, CheckpointDir: string; + RunResult: TGraphRunResult; + Confirm: string; + + // ── Subgrafo "polish_agent" ──────────────────────────────────────────── + // Um grafo compilado independente (seu próprio TAgentGraph, seu próprio + // TLLMNode, sem tools), embutido no grafo principal como um nó comum via + // ICompiledAgent.AsNode. Reescreve a resposta bruta do 'call_llm' de forma + // mais objetiva antes de virar a resposta final ao usuário. + // + // IMPORTANTE: um subgrafo não pode ter RequireApproval/InterruptBefore em + // nenhum nó seu — AsNode levantaria EGraphCompileError (aprovação humana + // aninhada não é suportada). Se o fluxo do subgrafo precisar de aprovação, + // ela deve ficar no nó do grafo PAI que o invoca (aqui, seria em + // 'polish_agent' do grafo principal, não dentro do PolishGraph). + PolishConfig: TAgentConfig; + PolishLLMNode: TLLMNode; + PolishGraph: TAgentGraph; + PolishAgent: ICompiledAgent; begin ReportMemoryLeaksOnShutdown := True; @@ -184,25 +212,51 @@ begin end; end; - Observer := TConsoleObserver.Create; - ToolsNode := TToolsNode.Create; - LLMNode := nil; + Observer := TConsoleObserver.Create; + ToolsNode := TToolsNode.Create; + LLMNode := nil; + PolishLLMNode := nil; try ToolsNode.RegisterProvider(TFileSystemTools.Create); LLMNode := TLLMNode.Create(ToolsNode.GetToolSchemas); - Checkpointer := TMemoryCheckpointer.Create; + // TFileCheckpointer em vez de TMemoryCheckpointer: o estado da thread + // sobrevive ao encerramento do processo — feche o GraphDemo, abra de + // novo, use a mesma ThreadId e o histórico continua de onde parou. + Checkpointer := TFileCheckpointer.Create; + CheckpointDir := TPath.Combine(TPath.GetTempPath, 'dext-ai-graph'); + + // ── Compila o subgrafo 'polish_agent' primeiro (grafo independente) ── + PolishConfig := Config; + PolishConfig.SystemPrompt := + 'Reescreva a última resposta do assistente de forma mais clara e ' + + 'objetiva para o usuário final. Mantenha os fatos exatamente como ' + + 'estão — não invente, não adicione e não remova informação.'; + PolishLLMNode := TLLMNode.Create(nil); // sem tools: só reescreve texto + PolishGraph := TAgentGraph.Create; + try + PolishAgent := PolishGraph + .AddNode('polish_llm', PolishLLMNode.AsHandler) + .SetEntryPoint('polish_llm') + .Compile(Provider, PolishConfig, Observer, nil); + finally + PolishGraph.Free; + end; + + // ── Grafo principal: embute o subgrafo como o nó 'polish_agent' ────── Graph := TAgentGraph.Create; try Agent := Graph .AddNode('call_llm', LLMNode.AsHandler) .AddNode('execute_tools', ToolsNode.AsHandler) + .AddNode('polish_agent', PolishAgent.AsNode) .SetEntryPoint('call_llm') .AddConditionalEdge('call_llm', - DefaultShouldContinue, - [TEdgeRoute.To_('execute_tools'), TEdgeRoute.ToEnd]) + ShouldContinueOrPolish, + [TEdgeRoute.To_('execute_tools'), TEdgeRoute.To_('polish_agent')]) .AddEdge('execute_tools', 'call_llm') + .AddEdge('polish_agent', GRAPH_END) .RequireApproval('execute_tools') .Compile(Provider, Config, Observer, Checkpointer); finally @@ -213,7 +267,9 @@ begin Writeln(' Dext.AI.Graph — Demo (estilo LangGraph)'); Writeln(Format(' Provider: %s | Model: %s', [Provider.ProviderName, Provider.ModelName])); - Writeln(' Digite sua pergunta. Enter em branco para sair.'); + Writeln(' Grafo: call_llm -> execute_tools (aprovação) -> polish_agent (subgrafo) -> fim'); + Writeln(' Checkpoint em disco: ' + CheckpointDir); + Writeln(' Digite sua pergunta, ":estado" para inspecionar a thread, ou Enter em branco para sair.'); Writeln('═══════════════════════════════════════════════════════'); Writeln; @@ -225,6 +281,20 @@ begin if Input.Trim = '' then Break; + if SameText(Input.Trim, ':estado') then + begin + var CurState := TAgentState(Agent.GetState(ThreadId)); + if CurState = nil then + Writeln(' (nenhum estado salvo ainda para esta thread)') + else + Writeln(Format( + ' nó atual=%s | iteração=%d | concluído=%s | mensagens=%d | pendências=%d', + [CurState.CurrentNode, CurState.Iteration, BoolToStr(CurState.IsDone, True), + Length(CurState.Messages), Length(CurState.PendingCalls)])); + Writeln; + Continue; + end; + Writeln; RunResult := Agent.Run(Input, ThreadId); @@ -258,7 +328,9 @@ begin until False; finally Agent := nil; + PolishAgent := nil; LLMNode.Free; + PolishLLMNode.Free; ToolsNode.Free; end; end. From 6eb09fa0b870d086b5f50ad1ca1d338d361db2c9 Mon Sep 17 00:00:00 2001 From: alepmedeiros Date: Mon, 31 Aug 2026 08:35:57 -0300 Subject: [PATCH 4/4] Document Dext.AI.Agent + Dext.AI.Graph as Book chapter 16 (EN + PT-BR) Adds Docs/Book/16-ai-agents and its Docs/Book.pt-br mirror, following the existing per-chapter README.md convention (see chapter 15 for MCP). Covers: quick starts for both Dext.AI.Agent (single-agent ReAct, LangChain-style) and Dext.AI.Graph (graph orchestration, LangGraph-style), the core type table, human-in-the-loop, checkpointing, and subgraphs via AsNode - plus an explicit LangGraph coverage table (what maps 1:1, what's a real gap: no typed per-graph state schema, no conditional entry point, no interrupt_after or dynamic interrupts, no update_state, no nested HITL, no state history/time-travel, no cross-thread store, streaming only via IAgentObserver callbacks, and a documented pitfall - a second AddEdge from the same source node is silently ignored rather than fanning out). Wires the new chapter into both Book/README.md and Book.pt-br/README.md TOCs and example tables, and annotates Docs/roadmap/ai-roadmap.md to point at what's actually implemented under the Dext.AI.Agent/Dext.AI.Graph names, without rewriting the original Dext.SemanticKernel-branded roadmap someone else wrote - just noting where the two now overlap. Co-Authored-By: Claude Sonnet 5 --- Docs/Book.pt-br/16-ai-agents/README.md | 251 +++++++++++++++++++++++++ Docs/Book.pt-br/README.md | 7 + Docs/Book/16-ai-agents/README.md | 251 +++++++++++++++++++++++++ Docs/Book/README.md | 7 + Docs/roadmap/ai-roadmap.md | 24 ++- 5 files changed, 535 insertions(+), 5 deletions(-) create mode 100644 Docs/Book.pt-br/16-ai-agents/README.md create mode 100644 Docs/Book/16-ai-agents/README.md diff --git a/Docs/Book.pt-br/16-ai-agents/README.md b/Docs/Book.pt-br/16-ai-agents/README.md new file mode 100644 index 00000000..0388edc0 --- /dev/null +++ b/Docs/Book.pt-br/16-ai-agents/README.md @@ -0,0 +1,251 @@ +# 🤖 Agentes de IA & Orquestração (Dext.AI.Agent + Dext.AI.Graph) + +O Dext traz duas camadas de orquestração construídas sobre o [Dext MCP](../15-mcp-server/README.md), para quem quer o modelo mental do LangChain/LangGraph em Delphi nativo, sem nenhuma dependência externa além da RTL: + +- **`Dext.AI.Agent`** — um loop ReAct de agente único (pense no `AgentExecutor` do **LangChain**): um provider, um conjunto de tools, uma conversa. +- **`Dext.AI.Graph`** — orquestração via grafo por cima disso (pense no `StateGraph` do **LangGraph**): nós e edges explícitos, roteamento condicional, estado com checkpoint entre turnos, aprovação humana (human-in-the-loop) e composição de sub-agentes. + +Use `Dext.AI.Agent` quando um loop simples "chama o LLM, executa tools, repete até terminar" já resolve. Use `Dext.AI.Graph` quando precisar de mais de uma etapa distinta (um roteador, uma etapa de revisão, vários agentes cooperando) ou precisar pausar e retomar entre turnos/processos. + +As duas camadas usam diretamente o padrão `TMCPToolProvider` / `[MCPTool]` / `[MCPParam]` do [capítulo de MCP](../15-mcp-server/README.md) — as mesmas tools declarativas que você exporia para um cliente de IA externo podem ser reaproveitadas como o conjunto de tools do próprio agente. + +--- + +## 📦 Onde fica cada coisa + +``` +Sources/AI/Agent/ +├── Dext.AI.Agent.Contracts.pas ILLMProvider, TLLMMessage, TAgentConfig, IAgentObserver +├── Dext.AI.Agent.Factory.pas TLLMFactory.CreateProvider(config) +├── Dext.AI.Agent.Runner.pas TAgentRunner (loop ReAct de agente único) +├── Dext.AI.Agent.Observer.pas IAgentObserver, TConsoleObserver +└── Providers/ + ├── Dext.AI.Agent.Provider.OpenAI.pas + ├── Dext.AI.Agent.Provider.Anthropic.pas + └── Dext.AI.Agent.Provider.Ollama.pas + +Sources/AI/Graph/ +├── Dext.AI.Graph.Contracts.pas ICompiledAgent, ICheckpointer, TNodeHandler, TNodeContext +├── Dext.AI.Graph.State.pas TAgentState (imutável) +├── Dext.AI.Graph.Edge.pas TEdge, TEdgeRoute, TEdgeCondition +├── Dext.AI.Graph.Graph.pas TAgentGraph (o StateGraph) +├── Dext.AI.Graph.Compiled.pas TCompiledAgent (Run/Resume/Cancel/GetState/AsNode) +├── Dext.AI.Graph.Checkpointer.pas TMemoryCheckpointer, TFileCheckpointer +└── Nodes/ + ├── Dext.AI.Graph.Node.LLM.pas TLLMNode (nó padrão que chama o LLM) + └── Dext.AI.Graph.Node.Tools.pas TToolsNode (nó padrão que executa tools) +``` + +--- + +## 🚀 Dext.AI.Agent — Início Rápido + +```pascal +uses + Dext.AI.Agent.Contracts, + Dext.AI.Agent.Factory, + Dext.AI.Agent.Observer, + Dext.AI.MCP.Tools; + +var + Config: TAgentConfig; + Provider: ILLMProvider; + Runner: TAgentRunner; + Result: TAgentResult; +begin + Config := TAgentConfig.OpenAI('gpt-4o'); + Config.ApiKey := GetEnvironmentVariable('OPENAI_API_KEY'); + Config.SystemPrompt := 'Você é um assistente útil. Use as tools para responder com precisão.'; + + Provider := TLLMFactory.CreateProvider(Config); + + Runner := TAgentRunner.Create(Provider, Config, TConsoleObserver.Create); + try + Runner.RegisterProvider(TMyToolProvider.Create); // qualquer TMCPToolProvider + Result := Runner.Run('Quantos arquivos .pas existem em Sources/AI/Graph?'); + Writeln(Result.FinalAnswer); + finally + Runner.Free; + end; +end; +``` + +`TAgentConfig` tem construtores de fábrica para os três providers nativos: + +```pascal +TAgentConfig.OpenAI('gpt-4o'); +TAgentConfig.Anthropic('claude-sonnet-4-6'); +TAgentConfig.Ollama('llama3.2'); // BaseUrl padrão: http://localhost:11434 +``` + +`TLLMFactory.CreateProvider(Config)` lê `Config.ProviderString` (`'openai:gpt-4o'`, `'anthropic:...'`, `'ollama:...'`) e devolve o `ILLMProvider` correspondente. `ILLMProvider` é uma interface de estratégia pequena — implemente a sua para qualquer provider que não venha pronto: + +```pascal +ILLMProvider = interface + function Complete(const AMessages: TArray; + const ATools: TArray): TLLMResponse; + function ProviderName: string; + function ModelName: string; +end; +``` + +`IAgentObserver` é chamado a cada passo do loop ReAct (`OnIterationStart`, `OnToolCall`, `OnToolResult`, `OnLLMResponse`, `OnFinished`) — implemente o seu próprio para transmitir o progresso para uma UI, um arquivo de log ou um endpoint SSE, em vez do `TConsoleObserver` (que escreve no console). + +**Exemplo completo funcionando:** [Examples/AI/AgentDemo](../../Examples/AI/AgentDemo/) + +--- + +## 🕸️ Dext.AI.Graph — Início Rápido + +A API do grafo espelha o `StateGraph` do LangGraph quase um para um: + +```pascal +LangGraph (Python) Dext.AI.Graph (Delphi) +────────────────────────── ──────────────────────────────── +StateGraph(State) → TAgentGraph.Create +graph.add_node(name, fn) → Graph.AddNode(name, Handler) +graph.set_entry_point(name) → Graph.SetEntryPoint(name) +graph.add_edge(a, b) → Graph.AddEdge(a, b) +graph.add_conditional_edges → Graph.AddConditionalEdge(from, cond, routes) +START / END → GRAPH_START / GRAPH_END +graph.compile() → Graph.Compile(Provider, Config, Observer, Checkpointer) +compiled.invoke(input) → Agent.Run(input, threadId) +MemorySaver → TMemoryCheckpointer +interrupt_before → InterruptBefore([...]) / .RequireApproval(node) +Command(resume=...) → Agent.Resume(threadId) +Subgraphs → ICompiledAgent.AsNode +``` + +Um grafo ReAct mínimo — `call_llm` chama o modelo, roteia para `execute_tools` se ele pediu uma tool, volta em loop, e para quando o modelo tem uma resposta final: + +```pascal +uses + Dext.AI.Graph.Contracts, Dext.AI.Graph.State, Dext.AI.Graph.Edge, + Dext.AI.Graph.Graph, Dext.AI.Graph.Compiled, Dext.AI.Graph.Checkpointer, + Dext.AI.Graph.Node.LLM, Dext.AI.Graph.Node.Tools; + +var + ToolsNode: TToolsNode; + LLMNode: TLLMNode; + Graph: TAgentGraph; + Agent: ICompiledAgent; + Result: TGraphRunResult; +begin + ToolsNode := TToolsNode.Create; + ToolsNode.RegisterProvider(TMyToolProvider.Create); + LLMNode := TLLMNode.Create(ToolsNode.GetToolSchemas); + + Graph := TAgentGraph.Create; + try + Agent := Graph + .AddNode('call_llm', LLMNode.AsHandler) + .AddNode('execute_tools', ToolsNode.AsHandler) + .SetEntryPoint('call_llm') + .AddConditionalEdge('call_llm', DefaultShouldContinue, + [TEdgeRoute.To_('execute_tools'), TEdgeRoute.ToEnd]) + .AddEdge('execute_tools', 'call_llm') + .Compile(Provider, Config, Observer, TMemoryCheckpointer.Create); + finally + Graph.Free; // TCompiledAgent copia nós/edges — seguro liberar logo após o Compile + end; + + Result := Agent.Run('Quantos arquivos .pas existem em Sources/AI/Graph?', 'thread-1'); + Writeln(Result.FinalAnswer); +end; +``` + +**Exemplo completo (todos os recursos abaixo ligados juntos):** [Examples/AI/GraphDemo](../../Examples/AI/GraphDemo/) + +### Conceitos principais + +| Tipo | Papel | +|---|---| +| `TAgentState` | Estado imutável que flui pelo grafo — mensagens, tool calls pendentes, nó atual, contador de iteração, metadata. Todo método `With*` devolve uma **nova** instância; nada é alterado no lugar. | +| `TNodeHandler` | `reference to function(const AState: TAgentState; const ACtx: TNodeContext): TAgentState` — um nó é só uma função de estado para estado. | +| `TEdge` | Fixa (`AddEdge`) ou condicional (`AddConditionalEdge`, guiada por um `TEdgeCondition` que inspeciona o estado e devolve o nome do próximo nó). | +| `ICompiledAgent` | O resultado do `Compile()` — `Run`, `Resume`, `Cancel`, `GetState`, `AsNode`. | +| `ICheckpointer` | Persiste o `TAgentState` de uma thread (como JSON) para que a mesma `AThreadId` possa ser retomada entre chamadas de `Run` — e, com `TFileCheckpointer`, entre reinícios do processo. | + +### Human-in-the-loop (aprovação humana) + +Marque um nó como exigindo aprovação — o grafo pausa **antes** de executá-lo e devolve `grsWaitingApproval` em vez de rodar: + +```pascal +Graph.RequireApproval('execute_tools'); +// equivalente: Graph.InterruptBefore(['execute_tools']); +``` + +```pascal +Result := Agent.Run(Input, ThreadId); +if Result.Status = grsWaitingApproval then +begin + Writeln('Nó pendente: ' + Result.PendingNode); + if UsuarioAprovou then + Result := Agent.Resume(ThreadId) + else + Agent.Cancel(ThreadId); +end; +``` + +`Resume` reexecuta o próprio nó pausado e continua o loop normalmente. `Cancel` apaga o checkpoint da thread. + +### Checkpointing + +```pascal +TMemoryCheckpointer.Create; // local ao processo, some ao sair +TFileCheckpointer.Create; // arquivos JSON em %TEMP%\dext-ai-graph +TFileCheckpointer.Create('C:\MeuCaminho'); // ou um caminho à sua escolha +``` + +Os dois implementam a interface `ICheckpointer`, pequena (`Save`/`Load`/`Exists`/`Delete`) — implemente a sua para persistir numa tabela de banco de dados em vez disso. + +### Subgrafos (`AsNode`) + +Um grafo compilado pode ser embutido como um único nó de outro grafo — é assim que se compõem sub-agentes independentes e testáveis separadamente (um agente "Fiscal" dentro de um grafo "ERP" maior, por exemplo), em vez de achatar os nós de cada sub-agente num único grafo gigante: + +```pascal +FiscalAgent := FiscalGraph.AddNode(...).SetEntryPoint(...).Compile(Provider, Config); + +ERPGraph.AddNode('fiscal_agent', FiscalAgent.AsNode); +``` + +`TAgentState` é um único tipo concreto em todos os grafos do Dext.AI.Graph (diferente dos schemas tipados por grafo do LangGraph), então não há etapa de tradução de estado na fronteira do subgrafo — o estado do pai passa direto, o subgrafo roda do seu próprio ponto de entrada até seu próprio `GRAPH_END`, e o estado resultante (incluindo toda mensagem que ele anexou) volta para o pai, que então decide o que acontece depois através das suas próprias edges. + +> **Restrição:** um subgrafo não pode declarar `RequireApproval`/`InterruptBefore` em nenhum nó seu — `AsNode` levanta `EGraphCompileError` imediatamente, em vez de pular a aprovação silenciosamente. Se alguma parte do fluxo do subgrafo precisar de aprovação humana, coloque `RequireApproval` no nó do grafo **pai** que envolve a chamada do subgrafo. Human-in-the-loop aninhado ainda não é suportado. + +--- + +## 🧭 Vem do LangGraph? O que está coberto, o que não está + +| LangGraph | Dext.AI.Graph | +|---|---| +| `StateGraph` | ✅ `TAgentGraph` | +| funções de nó | ✅ `TNodeHandler` | +| `add_edge` / `add_conditional_edges` | ✅ `AddEdge` / `AddConditionalEdge` | +| `compile()` | ✅ `Compile()` | +| `MemorySaver` | ✅ `TMemoryCheckpointer` / `TFileCheckpointer` | +| multi-turn via `thread_id` | ✅ `AThreadId` | +| `interrupt_before` + resume/cancel | ✅ `RequireApproval` / `InterruptBefore` + `Resume` / `Cancel` | +| Subgrafos | ✅ `ICompiledAgent.AsNode` | +| `recursion_limit` | ✅ `MaxIterations` | +| Schema de estado tipado por grafo | ❌ `TAgentState` é um tipo fixo único; dados extras vão no `Metadata` (string→string) | +| Entry point condicional/dinâmico | ❌ `GRAPH_START` existe só como nome reservado; a entrada é sempre fixa via `SetEntryPoint` | +| `interrupt_after` | ❌ só "antes" é suportado | +| Interrupts dinâmicos (`interrupt()` dentro de um nó) | ❌ interrupts precisam ser declarados estaticamente no grafo | +| `update_state` | ❌ `GetState` é somente leitura; não há como editar o estado pausado antes do `Resume` | +| Human-in-the-loop aninhado | ❌ explicitamente rejeitado pelo `AsNode` (falha alto, não silenciosamente) | +| `get_state_history` / time travel | ❌ o checkpointer guarda só o último estado por thread | +| `Store` (memória de longo prazo entre threads) | ❌ persistência é só por thread | +| `stream_mode` (streaming de primeira classe) | ⚠️ parcial — `IAgentObserver` dá callbacks síncronos, não um stream/generator | +| Fan-out / ramos paralelos (`Send`) | ⚠️ **armadilha**: adicionar mais de um `AddEdge` a partir do mesmo nó de origem não é erro — só a *primeira* é usada, o resto é ignorado silenciosamente. Não há fan-out paralelo automático. | +| Retry policy / cache de resultado por nó | ❌ não implementado | +| `Command` (nó devolve roteamento + atualização de estado juntos) | ❌ o roteamento sempre passa pelas edges | + +Se o seu caso de uso precisar de ramos paralelos de verdade, histórico de estado para auditoria, ou memória entre threads, esses são gaps reais hoje, não só documentação faltando. + +--- + +## 📂 Exemplos + +- **[AgentDemo](../../Examples/AI/AgentDemo/)** — loop ReAct de agente único com tools de sistema de arquivos. +- **[GraphDemo](../../Examples/AI/GraphDemo/)** — todo o conjunto de recursos do grafo ligado junto: roteamento condicional, `RequireApproval` + `Resume`/`Cancel`, persistência via `TFileCheckpointer` entre reinícios, e um subgrafo `polish_agent` embutido via `AsNode`. diff --git a/Docs/Book.pt-br/README.md b/Docs/Book.pt-br/README.md index 1363555e..34784d58 100644 --- a/Docs/Book.pt-br/README.md +++ b/Docs/Book.pt-br/README.md @@ -118,6 +118,11 @@ - [Skills de IA](13-ai-assistants/README.md) - Instruções nativas para integração de IA - [Servidor MCP](15-mcp-server/README.md) - Implementação nativa do Model Context Protocol +#### [16. Agentes de IA & Orquestração](16-ai-agents/README.md) ⭐ NOVO + +- Dext.AI.Agent - Loop ReAct de agente único (estilo LangChain) +- Dext.AI.Graph - Orquestração via grafo, checkpointing, human-in-the-loop, subgrafos (estilo LangGraph) + --- ### Apêndice @@ -148,6 +153,8 @@ Cada capítulo referencia exemplos funcionais do diretório `Examples/` | [Orm.EntityDemo](../../Examples/Orm.EntityDemo/) | ORM Básico | | [Hubs](../../Examples/Hubs/) | SignalR Tempo Real | | [Desktop.MVVM.CustomerCRUD](../../Examples/Desktop.MVVM.CustomerCRUD/) | Navigator, MVVM, Testes | +| [AI.AgentDemo](../../Examples/AI/AgentDemo/) | Dext.AI.Agent, loop ReAct | +| [AI.GraphDemo](../../Examples/AI/GraphDemo/) | Dext.AI.Graph, checkpointing, HITL, subgrafos | --- diff --git a/Docs/Book/16-ai-agents/README.md b/Docs/Book/16-ai-agents/README.md new file mode 100644 index 00000000..a6c23553 --- /dev/null +++ b/Docs/Book/16-ai-agents/README.md @@ -0,0 +1,251 @@ +# 🤖 AI Agents & Orchestration (Dext.AI.Agent + Dext.AI.Graph) + +Dext ships two orchestration layers on top of [Dext MCP](../15-mcp-server/README.md), for teams who want the LangChain/LangGraph mental model in native Delphi, with zero external dependencies beyond the RTL: + +- **`Dext.AI.Agent`** — a single-agent ReAct loop (think **LangChain**'s `AgentExecutor`): one provider, one tool set, one conversation. +- **`Dext.AI.Graph`** — graph-based orchestration on top of it (think **LangGraph**'s `StateGraph`): explicit nodes and edges, conditional routing, checkpointed multi-turn state, human-in-the-loop approval, and sub-agent composition. + +Use `Dext.AI.Agent` when a plain "call the LLM, run tools, repeat until done" loop is enough. Reach for `Dext.AI.Graph` when you need more than one distinct step (a router, a review step, multiple cooperating agents) or you need to pause and resume across turns/processes. + +Both build directly on the `TMCPToolProvider` / `[MCPTool]` / `[MCPParam]` pattern from the [MCP chapter](../15-mcp-server/README.md) — the same declarative tools you'd expose to an external AI client can be reused as an agent's own tool set. + +--- + +## 📦 Where things live + +``` +Sources/AI/Agent/ +├── Dext.AI.Agent.Contracts.pas ILLMProvider, TLLMMessage, TAgentConfig, IAgentObserver +├── Dext.AI.Agent.Factory.pas TLLMFactory.CreateProvider(config) +├── Dext.AI.Agent.Runner.pas TAgentRunner (single-agent ReAct loop) +├── Dext.AI.Agent.Observer.pas IAgentObserver, TConsoleObserver +└── Providers/ + ├── Dext.AI.Agent.Provider.OpenAI.pas + ├── Dext.AI.Agent.Provider.Anthropic.pas + └── Dext.AI.Agent.Provider.Ollama.pas + +Sources/AI/Graph/ +├── Dext.AI.Graph.Contracts.pas ICompiledAgent, ICheckpointer, TNodeHandler, TNodeContext +├── Dext.AI.Graph.State.pas TAgentState (immutable) +├── Dext.AI.Graph.Edge.pas TEdge, TEdgeRoute, TEdgeCondition +├── Dext.AI.Graph.Graph.pas TAgentGraph (the StateGraph) +├── Dext.AI.Graph.Compiled.pas TCompiledAgent (Run/Resume/Cancel/GetState/AsNode) +├── Dext.AI.Graph.Checkpointer.pas TMemoryCheckpointer, TFileCheckpointer +└── Nodes/ + ├── Dext.AI.Graph.Node.LLM.pas TLLMNode (default LLM-calling node) + └── Dext.AI.Graph.Node.Tools.pas TToolsNode (default tool-executing node) +``` + +--- + +## 🚀 Dext.AI.Agent — Quick Start + +```pascal +uses + Dext.AI.Agent.Contracts, + Dext.AI.Agent.Factory, + Dext.AI.Agent.Observer, + Dext.AI.MCP.Tools; + +var + Config: TAgentConfig; + Provider: ILLMProvider; + Runner: TAgentRunner; + Result: TAgentResult; +begin + Config := TAgentConfig.OpenAI('gpt-4o'); + Config.ApiKey := GetEnvironmentVariable('OPENAI_API_KEY'); + Config.SystemPrompt := 'You are a helpful assistant. Use tools to answer accurately.'; + + Provider := TLLMFactory.CreateProvider(Config); + + Runner := TAgentRunner.Create(Provider, Config, TConsoleObserver.Create); + try + Runner.RegisterProvider(TMyToolProvider.Create); // any TMCPToolProvider + Result := Runner.Run('How many .pas files are in Sources/AI/Graph?'); + Writeln(Result.FinalAnswer); + finally + Runner.Free; + end; +end; +``` + +`TAgentConfig` has factory constructors for the three built-in providers: + +```pascal +TAgentConfig.OpenAI('gpt-4o'); +TAgentConfig.Anthropic('claude-sonnet-4-6'); +TAgentConfig.Ollama('llama3.2'); // BaseUrl defaults to http://localhost:11434 +``` + +`TLLMFactory.CreateProvider(Config)` reads `Config.ProviderString` (`'openai:gpt-4o'`, `'anthropic:...'`, `'ollama:...'`) and returns the matching `ILLMProvider`. `ILLMProvider` is a single, small strategy interface — implement it yourself for any provider not built in: + +```pascal +ILLMProvider = interface + function Complete(const AMessages: TArray; + const ATools: TArray): TLLMResponse; + function ProviderName: string; + function ModelName: string; +end; +``` + +`IAgentObserver` gets called at every step of the ReAct loop (`OnIterationStart`, `OnToolCall`, `OnToolResult`, `OnLLMResponse`, `OnFinished`) — implement your own to stream progress into a UI, log file, or SSE endpoint instead of `TConsoleObserver`'s stdout. + +**Full working example:** [Examples/AI/AgentDemo](../../Examples/AI/AgentDemo/) + +--- + +## 🕸️ Dext.AI.Graph — Quick Start + +The graph API mirrors LangGraph's `StateGraph` almost one to one: + +```pascal +LangGraph (Python) Dext.AI.Graph (Delphi) +────────────────────────── ──────────────────────────────── +StateGraph(State) → TAgentGraph.Create +graph.add_node(name, fn) → Graph.AddNode(name, Handler) +graph.set_entry_point(name) → Graph.SetEntryPoint(name) +graph.add_edge(a, b) → Graph.AddEdge(a, b) +graph.add_conditional_edges → Graph.AddConditionalEdge(from, cond, routes) +START / END → GRAPH_START / GRAPH_END +graph.compile() → Graph.Compile(Provider, Config, Observer, Checkpointer) +compiled.invoke(input) → Agent.Run(input, threadId) +MemorySaver → TMemoryCheckpointer +interrupt_before → InterruptBefore([...]) / .RequireApproval(node) +Command(resume=...) → Agent.Resume(threadId) +Subgraphs → ICompiledAgent.AsNode +``` + +A minimal ReAct graph — `call_llm` calls the model, routes to `execute_tools` if it asked for a tool, loops back, and stops when the model has a final answer: + +```pascal +uses + Dext.AI.Graph.Contracts, Dext.AI.Graph.State, Dext.AI.Graph.Edge, + Dext.AI.Graph.Graph, Dext.AI.Graph.Compiled, Dext.AI.Graph.Checkpointer, + Dext.AI.Graph.Node.LLM, Dext.AI.Graph.Node.Tools; + +var + ToolsNode: TToolsNode; + LLMNode: TLLMNode; + Graph: TAgentGraph; + Agent: ICompiledAgent; + Result: TGraphRunResult; +begin + ToolsNode := TToolsNode.Create; + ToolsNode.RegisterProvider(TMyToolProvider.Create); + LLMNode := TLLMNode.Create(ToolsNode.GetToolSchemas); + + Graph := TAgentGraph.Create; + try + Agent := Graph + .AddNode('call_llm', LLMNode.AsHandler) + .AddNode('execute_tools', ToolsNode.AsHandler) + .SetEntryPoint('call_llm') + .AddConditionalEdge('call_llm', DefaultShouldContinue, + [TEdgeRoute.To_('execute_tools'), TEdgeRoute.ToEnd]) + .AddEdge('execute_tools', 'call_llm') + .Compile(Provider, Config, Observer, TMemoryCheckpointer.Create); + finally + Graph.Free; // TCompiledAgent copies nodes/edges — safe to free right after Compile + end; + + Result := Agent.Run('How many .pas files are in Sources/AI/Graph?', 'thread-1'); + Writeln(Result.FinalAnswer); +end; +``` + +**Full working example (all features below wired together):** [Examples/AI/GraphDemo](../../Examples/AI/GraphDemo/) + +### Core concepts + +| Type | Role | +|---|---| +| `TAgentState` | Immutable state that flows through the graph — messages, pending tool calls, current node, iteration count, metadata. Every `With*` method returns a **new** instance; nothing mutates in place. | +| `TNodeHandler` | `reference to function(const AState: TAgentState; const ACtx: TNodeContext): TAgentState` — a node is just a function from state to state. | +| `TEdge` | Fixed (`AddEdge`) or conditional (`AddConditionalEdge`, driven by a `TEdgeCondition` that inspects state and returns the next node's name). | +| `ICompiledAgent` | The result of `Compile()` — `Run`, `Resume`, `Cancel`, `GetState`, `AsNode`. | +| `ICheckpointer` | Persists a thread's `TAgentState` (as JSON) so the same `AThreadId` can be resumed across `Run` calls — and, with `TFileCheckpointer`, across process restarts. | + +### Human-in-the-loop + +Mark a node as requiring approval — the graph pauses **before** running it and returns `grsWaitingApproval` instead of executing: + +```pascal +Graph.RequireApproval('execute_tools'); +// equivalent: Graph.InterruptBefore(['execute_tools']); +``` + +```pascal +Result := Agent.Run(Input, ThreadId); +if Result.Status = grsWaitingApproval then +begin + Writeln('Pending node: ' + Result.PendingNode); + if UserApproves then + Result := Agent.Resume(ThreadId) + else + Agent.Cancel(ThreadId); +end; +``` + +`Resume` re-executes the paused node itself, then continues the loop normally. `Cancel` deletes the thread's checkpoint. + +### Checkpointing + +```pascal +TMemoryCheckpointer.Create; // process-local, gone on exit +TFileCheckpointer.Create; // JSON files under %TEMP%\dext-ai-graph +TFileCheckpointer.Create('C:\MyPath'); // or a path you choose +``` + +Both implement the tiny `ICheckpointer` interface (`Save`/`Load`/`Exists`/`Delete`) — implement your own to persist to a database table instead. + +### Subgraphs (`AsNode`) + +A compiled graph can be embedded as a single node of another graph — this is how you compose independent, separately-tested sub-agents (a "Fiscal" agent inside a larger "ERP" graph, for example) instead of flattening every sub-agent's nodes into one giant graph: + +```pascal +FiscalAgent := FiscalGraph.AddNode(...).SetEntryPoint(...).Compile(Provider, Config); + +ERPGraph.AddNode('fiscal_agent', FiscalAgent.AsNode); +``` + +`TAgentState` is a single concrete type across every graph in Dext.AI.Graph (unlike LangGraph's per-graph typed schemas), so there is no state-translation step at the subgraph boundary — the parent's state is passed straight through, the subgraph runs from its own entry point to its own `GRAPH_END`, and the resulting state (including every message it appended) flows back to the parent, which then decides what happens next via its own edges. + +> **Constraint:** a subgraph cannot itself declare `RequireApproval`/`InterruptBefore` on any node — `AsNode` raises `EGraphCompileError` immediately rather than silently skipping the approval step. If part of a subgraph's flow needs human approval, put `RequireApproval` on the **parent** node that wraps the subgraph call. Nested human-in-the-loop isn't supported yet. + +--- + +## 🧭 Coming from LangGraph? What's covered, what isn't + +| LangGraph | Dext.AI.Graph | +|---|---| +| `StateGraph` | ✅ `TAgentGraph` | +| node functions | ✅ `TNodeHandler` | +| `add_edge` / `add_conditional_edges` | ✅ `AddEdge` / `AddConditionalEdge` | +| `compile()` | ✅ `Compile()` | +| `MemorySaver` | ✅ `TMemoryCheckpointer` / `TFileCheckpointer` | +| `thread_id` multi-turn | ✅ `AThreadId` | +| `interrupt_before` + resume/cancel | ✅ `RequireApproval` / `InterruptBefore` + `Resume` / `Cancel` | +| Subgraphs | ✅ `ICompiledAgent.AsNode` | +| `recursion_limit` | ✅ `MaxIterations` | +| Typed per-graph state schema | ❌ `TAgentState` is one fixed type; extra data goes in `Metadata` (string→string) | +| Conditional/dynamic entry point | ❌ `GRAPH_START` exists only as a reserved name; entry is always fixed via `SetEntryPoint` | +| `interrupt_after` | ❌ only "before" is supported | +| Dynamic interrupts (`interrupt()` inside a node) | ❌ interrupts must be statically declared on the graph | +| `update_state` | ❌ `GetState` is read-only; no way to edit paused state before `Resume` | +| Nested human-in-the-loop | ❌ explicitly rejected by `AsNode` (fails loud, not silently) | +| `get_state_history` / time travel | ❌ the checkpointer keeps only the latest state per thread | +| `Store` (cross-thread long-term memory) | ❌ persistence is per-thread only | +| `stream_mode` (first-class streaming) | ⚠️ partial — `IAgentObserver` gives synchronous callbacks, not a stream/generator | +| Fan-out / parallel branches (`Send`) | ⚠️ **pitfall**: adding more than one `AddEdge` from the same source node is not an error — only the *first* one is ever used, the rest are silently ignored. There is no automatic parallel fan-out. | +| Retry policy / node result caching | ❌ not implemented | +| `Command` (node returns routing + state update together) | ❌ routing always goes through edges | + +If your use case needs real parallel branches, state history for auditing, or cross-thread memory, those are gaps today, not just missing docs. + +--- + +## 📂 Example Projects + +- **[AgentDemo](../../Examples/AI/AgentDemo/)** — single-agent ReAct loop with filesystem tools. +- **[GraphDemo](../../Examples/AI/GraphDemo/)** — the full graph feature set wired together: conditional routing, `RequireApproval` + `Resume`/`Cancel`, `TFileCheckpointer` persistence across restarts, and a `polish_agent` subgraph embedded via `AsNode`. diff --git a/Docs/Book/README.md b/Docs/Book/README.md index d80ac960..0a74fb37 100644 --- a/Docs/Book/README.md +++ b/Docs/Book/README.md @@ -127,6 +127,11 @@ - [AI Skills](13-ai-assistants/README.md) - Native AI integration skills - [MCP Server](15-mcp-server/README.md) - Model Context Protocol implementation +#### [16. AI Agents & Orchestration](16-ai-agents/README.md) ⭐ NEW + +- Dext.AI.Agent - Single-agent ReAct loop (LangChain-style) +- Dext.AI.Graph - Graph orchestration, checkpointing, human-in-the-loop, subgraphs (LangGraph-style) + --- ### Appendix @@ -154,6 +159,8 @@ Each chapter references working examples from the `Examples/` directory: | [Orm.EntityDemo](../../Examples/Orm.EntityDemo/) | ORM Basics | | [Hubs](../../Examples/Hubs/) | Real-Time SignalR | | [Desktop.MVVM.CustomerCRUD](../../Examples/Desktop.MVVM.CustomerCRUD/) | Navigator, MVVM, Testing | +| [AI.AgentDemo](../../Examples/AI/AgentDemo/) | Dext.AI.Agent, ReAct loop | +| [AI.GraphDemo](../../Examples/AI/GraphDemo/) | Dext.AI.Graph, checkpointing, HITL, subgraphs | --- diff --git a/Docs/roadmap/ai-roadmap.md b/Docs/roadmap/ai-roadmap.md index 8c2a0dc8..1d31e168 100644 --- a/Docs/roadmap/ai-roadmap.md +++ b/Docs/roadmap/ai-roadmap.md @@ -20,17 +20,31 @@ Permite expor ferramentas e recursos do Dext para agentes de IA externos (como C Inspirado no Microsoft Semantic Kernel, este módulo será o "cérebro" para integrar LLMs com código nativo. +> **Nota (2026-08):** o núcleo de orquestração (chamada de LLM multi-provider, function +> calling via RTTI, loop de agente com tools, e agora orquestração via grafo estilo +> LangGraph) já foi implementado — mas sob os nomes `Dext.AI.Agent` e `Dext.AI.Graph`, +> não `Dext.SemanticKernel`. Ver [Capítulo 16 do Book](../Book/16-ai-agents/README.md). +> Os itens abaixo marcados como concluídos refletem o que já existe sob esses nomes; +> o restante (embeddings, planner dedicado, plugins ao estilo Semantic Kernel) continua +> em aberto. Mantendo o roadmap original sem reescrever a decisão de nomenclatura — +> só sinalizando onde o que já existe se encaixa. + ### 1. Core Abstractions -- [ ] **IChatCompletion**: Interface unificada para chat (OpenAI, Azure OpenAI, Anthropic, Ollama). +- [x] **IChatCompletion**: interface unificada de chat multi-provider — implementada como + `ILLMProvider` (`Dext.AI.Agent.Contracts`), com providers para OpenAI, Anthropic e Ollama. - [ ] **ITextEmbedding**: Interface para geração de vetores (embeddings). - [ ] **Prompt Templates**: Engine para renderizar prompts dinâmicos com variáveis (`"Olá {{name}}, ajude-me com..."`). ### 2. Plugins & Native Functions (The "Glue") A capacidade de LLMs chamarem código Delphi (Function Calling). -- [ ] **Native Plugins**: Expor classes Delphi como "Skills" para a IA usando RTTI. - - Atributos: `[SKFunction]`, `[SKDescription]`. - - Geração automática de Schema JSON para a LLM entender a função. -- [ ] **Planner**: Um agente que decide quais funções chamar para resolver uma solicitação complexa do usuário. +- [x] **Native Plugins**: expor classes Delphi como tools para a IA via RTTI — implementado + como `TMCPToolProvider` + `[MCPTool]`/`[MCPParam]` (`Dext.AI.MCP.*`), reaproveitado + diretamente por `Dext.AI.Agent`/`Dext.AI.Graph` como o conjunto de tools do agente. + - Geração automática de Schema JSON para a LLM entender a função. ✅ +- [x] **Planner**: um agente que decide quais funções chamar para resolver uma solicitação + complexa — implementado como o loop ReAct de `TAgentRunner` (agente único) e como o + grafo `TAgentGraph`/`ICompiledAgent` (roteamento condicional, checkpoint, + human-in-the-loop, subgrafos via `AsNode`) para orquestração multi-etapa/multi-agente. ### 3. Structured Output (Pydantic-like) - [ ] **Schema Validation**: Garantir que a IA retorne JSON válido que mapeia exatamente para um `record` ou `class` Delphi.