Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,12 +111,12 @@ RAG / Agents / MCP — higher-level orchestration layers
| `Source/Design/UMakerAi.ParamsRegistry.pas` | Implemented | `TAiChatFactory`, `TAiEmbeddingFactory` singletons |
| `Source/Core/uMakerAi.Utils.CodeExtractor.pas` | Implemented | Extract code blocks from Markdown LLM responses |
| `Source/Core/uMakerAi.Version.inc` | Implemented | Version constants and feature flags |
| `Source/Tools/uMakerAi.Tools.*.pas` | **Stubs** | Shell, ComputerUse, TextEditor, Functions — to be ported |
| `Source/Chat/` | **Empty** | All LLM provider drivers — to be ported |
| `Source/Agents/` | **Empty** | Agent orchestration — to be ported |
| `Source/RAG/` | **Empty** | Vector + Graph RAG — to be ported |
| `Source/MCPClient/` | **Empty** | MCP client — to be ported |
| `Source/MCPServer/` | **Empty** | MCP server — to be ported |
| `Source/Tools/uMakerAi.Tools.*.pas` | **Implemented** | Shell, ComputerUse, TextEditor, Functions — Fully ported |
| `Source/Chat/` | **Implemented** | All LLM provider drivers — Fully ported |
| `Source/Agents/` | **Implemented** | Agent orchestration — Fully ported |
| `Source/RAG/` | **Implemented** | Vector + Graph RAG — Fully ported |
| `Source/MCPClient/` | **Implemented** | MCP client — Fully ported |
| `Source/MCPServer/` | **Implemented** | MCP server — Fully ported |

### Design Patterns (from Delphi original, apply same patterns in FPC)

Expand Down
2 changes: 0 additions & 2 deletions Demos/demo_rag_graph.pas
Original file line number Diff line number Diff line change
Expand Up @@ -70,11 +70,9 @@ procedure AddNodeAndEmbed(Graph: TAiRagGraph; Emb: TAiEmbeddings;
// Nodos — tecnologias
NTransformer, NRLHF, NSFT: TAiRagGraphNode;

Nodes : TNodeArray;
I : Integer;
GqlRes : string;
Path : TObjectArray;
PathNode: TAiRagGraphNode;
PathEdge: TAiRagGraphEdge;

// Para Match programatico
Expand Down
165 changes: 165 additions & 0 deletions Demos/test_tool_leak.pas
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
// MIT License - Copyright (c) 2024-2026 Gustavo Enriquez
// test_tool_leak.pas — Demostración: Diseño no-propietario de tools
//
// Las tools NO son propiedad del chat (diseño real del framework).
// TAiChatSim.Destroy solo libera recursos propios (FSomeList).
// Las 11 tool instances deben liberarse explícitamente desde afuera
// mediante FreeTools, porque el chat solo tiene referencias NO
// propietarias a ellas.
//
// Este demo replica el patrón con contador de instancias para
// demostrar que no hay leak cuando se liberan las tools externamente.

program test_tool_leak;

{$mode objfpc}{$H+}

uses
uDemoHelper,
SysUtils, Classes;

type
// ── Clases "tool" simuladas ──────────────────────────────────────────────
TBaseTool = class
private
FName: string;
public
constructor Create(const AName: string);
destructor Destroy; override;
end;

TSpeechToolSim = class(TBaseTool);
TImageToolSim = class(TBaseTool);
TVideoToolSim = class(TBaseTool);
TWebSearchToolSim = class(TBaseTool);
TVisionToolSim = class(TBaseTool);
TPdfToolSim = class(TBaseTool);
TReportToolSim = class(TBaseTool);
TShellToolSim = class(TBaseTool);
TTextEditorToolSim = class(TBaseTool);
TComputerUseToolSim = class(TBaseTool);
TAiFunctionsSim = class(TBaseTool);

var
GAllocCount: Integer = 0;

constructor TBaseTool.Create(const AName: string);
begin
FName := AName;
InterlockedIncrement(GAllocCount);
WriteLn(' [ALLOC] ', FName, ' -> vivas: ', GAllocCount);
end;

destructor TBaseTool.Destroy;
begin
InterlockedDecrement(GAllocCount);
WriteLn(' [FREE] ', FName, ' -> vivas: ', GAllocCount);
inherited Destroy;
end;

type
// ── Clase que replica TAiChat (solo lo relevante) ───────────────────────
TAiChatSim = class
private
FSpeechTool : TSpeechToolSim;
FImageTool : TImageToolSim;
FVideoTool : TVideoToolSim;
FWebSearchTool : TWebSearchToolSim;
FVisionTool : TVisionToolSim;
FPdfTool : TPdfToolSim;
FReportTool : TReportToolSim;
FShellTool : TShellToolSim;
FTextEditorTool : TTextEditorToolSim;
FComputerUseTool : TComputerUseToolSim;
FAiFunctions : TAiFunctionsSim;
FSomeList : TStringList;
public
constructor Create;
destructor Destroy; override;
procedure FreeTools;
procedure AssignTools;
end;

constructor TAiChatSim.Create;
begin
FSomeList := TStringList.Create;
FSomeList.Add('datos');
// Tools empiezan en nil (como en TAiChat.Create)
end;

// ── Diseño no-propietario ────────────────────────────────────────────────
// El chat NO posee las tools. Destroy solo libera recursos propios.
// Las tools deben liberarse explícitamente desde afuera mediante FreeTools.
destructor TAiChatSim.Destroy;
begin
FSomeList.Free;
inherited Destroy;
end;

procedure TAiChatSim.FreeTools;
begin
FreeAndNil(FSpeechTool);
FreeAndNil(FImageTool);
FreeAndNil(FVideoTool);
FreeAndNil(FWebSearchTool);
FreeAndNil(FVisionTool);
FreeAndNil(FPdfTool);
FreeAndNil(FReportTool);
FreeAndNil(FShellTool);
FreeAndNil(FTextEditorTool);
FreeAndNil(FComputerUseTool);
FreeAndNil(FAiFunctions);
end;

procedure TAiChatSim.AssignTools;
begin
WriteLn;
WriteLn(' Asignando 11 tool instances...');
FSpeechTool := TSpeechToolSim.Create('SpeechTool');
FImageTool := TImageToolSim.Create('ImageTool');
FVideoTool := TVideoToolSim.Create('VideoTool');
FWebSearchTool := TWebSearchToolSim.Create('WebSearchTool');
FVisionTool := TVisionToolSim.Create('VisionTool');
FPdfTool := TPdfToolSim.Create('PdfTool');
FReportTool := TReportToolSim.Create('ReportTool');
FShellTool := TShellToolSim.Create('ShellTool');
FTextEditorTool := TTextEditorToolSim.Create('TextEditorTool');
FComputerUseTool := TComputerUseToolSim.Create('ComputerUseTool');
FAiFunctions := TAiFunctionsSim.Create('AiFunctions');
end;

var
Chat: TAiChatSim;
begin
WriteLn('══════════════════════════════════════════════════════════════');
WriteLn(' Demo: Diseño no-propietario de tools');
WriteLn(' Las tools NO son propiedad del chat; se liberan externamente');
WriteLn('══════════════════════════════════════════════════════════════');
WriteLn;
WriteLn(' Escenario:');
WriteLn(' 1. Crear TAiChatSim');
WriteLn(' 2. Asignar 11 tool instances');
WriteLn(' 3. Liberar tools explícitamente (FreeTools) — el llamador es dueño');
WriteLn(' 4. Destruir el objeto (Destroy solo libera recursos propios)');
WriteLn;

Chat := TAiChatSim.Create;
Chat.AssignTools;

WriteLn;
WriteLn(' Liberando tools explícitamente (FreeTools)...');
Chat.FreeTools;

WriteLn;
WriteLn(' Destruyendo TAiChatSim (solo FSomeList + inherited)...');
Chat.Free;

WriteLn;
if GAllocCount = 0 then
WriteLn(' ✅ RESULTADO: 0 instancias vivas — SIN LEAK')
else
WriteLn(' ❌ RESULTADO: ', GAllocCount, ' instancias vivas — HAY LEAK');

WriteLn;
WriteLn('══════════════════════════════════════════════════════════════');
end.
44 changes: 29 additions & 15 deletions Source/Core/uMakerAi.Chat.pas
Original file line number Diff line number Diff line change
Expand Up @@ -561,6 +561,9 @@ implementation

uses UMakerAi.ParamsRegistry, TypInfo;

var
LogDebugCS: TCriticalSection;

// ===========================================================================
// TToolCallThread — ejecuta una tool call en un thread independiente
// Permite paralelizar varias tool calls (equivalente a TTask de Delphi)
Expand Down Expand Up @@ -691,24 +694,29 @@ procedure LogDebug(const Mensaje: string);
Path : string;
begin
if not MakerAiDebugLogEnabled then Exit;
LogDebugCS.Enter;
try
if MakerAiDebugLogPath <> '' then
Path := MakerAiDebugLogPath
else
Path := GetTempDir + 'makerai_debug.log';
if FileExists(Path) then
FS := TFileStream.Create(Path, fmOpenWrite or fmShareDenyNone)
else
FS := TFileStream.Create(Path, fmCreate or fmShareDenyNone);
try
FS.Seek(0, soEnd);
S := Mensaje + LineEnding;
FS.WriteBuffer(Pointer(S)^, Length(S));
finally
FS.Free;
if MakerAiDebugLogPath <> '' then
Path := MakerAiDebugLogPath
else
Path := GetTempDir + 'makerai_debug.log';
if FileExists(Path) then
FS := TFileStream.Create(Path, fmOpenWrite or fmShareDenyNone)
else
FS := TFileStream.Create(Path, fmCreate or fmShareDenyNone);
try
FS.Seek(0, soEnd);
S := Mensaje + LineEnding;
FS.WriteBuffer(Pointer(S)^, Length(S));
finally
FS.Free;
end;
except
// Silencioso — el log nunca debe interrumpir la request
end;
except
// Silencioso — el log nunca debe interrumpir la request
finally
LogDebugCS.Leave;
end;
end;

Expand Down Expand Up @@ -2891,4 +2899,10 @@ procedure TAiChat.SetThinking_tokens(const Value: Integer);
procedure TAiChat.SetLastError(const Value: string);
begin FLastError := Value; end;

initialization
LogDebugCS := TCriticalSection.Create;

finalization
LogDebugCS.Free;

end.
10 changes: 10 additions & 0 deletions Source/Core/uMakerAi.Core.pas
Original file line number Diff line number Diff line change
Expand Up @@ -606,6 +606,7 @@ destructor TAiMediaFile.Destroy;

procedure TAiMediaFile.Clear;
begin
if not Assigned(FContent) then FContent := TMemoryStream.Create;
FContent.Clear;
FContentLoaded := False;
Ffilename := '';
Expand Down Expand Up @@ -682,6 +683,7 @@ procedure TAiMediaFile.DownloadFileFromUrl(Url: string);
// TODO: manejar redirects y HTTPS (requiere opensslsockets)
Client.Get(Url, Response);

if not Assigned(FContent) then FContent := TMemoryStream.Create;
FContent.Clear;
Response.Position := 0;
FContent.CopyFrom(Response, 0);
Expand All @@ -708,6 +710,7 @@ function TAiMediaFile.GetContent: TMemoryStream;

function TAiMediaFile.GetBase64: string;
begin
if not Assigned(FContent) then FContent := TMemoryStream.Create;
FContent.Position := 0;
Result := EncodeBase64(FContent.Memory, FContent.Size);
Result := StringReplace(Result, LineEnding, '', [rfReplaceAll]);
Expand All @@ -721,6 +724,7 @@ procedure TAiMediaFile.SetBase64(const Value: string);

function TAiMediaFile.GetBytes: Integer;
begin
if not Assigned(FContent) then FContent := TMemoryStream.Create;
Result := FContent.Size;
end;

Expand All @@ -741,6 +745,7 @@ procedure TAiMediaFile.LoadFromFile(aFileName: string);
begin
if FileExists(aFileName) then
begin
if not Assigned(FContent) then FContent := TMemoryStream.Create;
FContent.Clear;
FContent.LoadFromFile(aFileName);
FContentLoaded := True;
Expand All @@ -756,6 +761,7 @@ procedure TAiMediaFile.LoadFromUrl(aUrl: string);
LPos: Integer;
begin
FUrlMedia := aUrl;
if not Assigned(FContent) then FContent := TMemoryStream.Create;
FContent.Clear;
GetContent;

Expand All @@ -782,6 +788,7 @@ procedure TAiMediaFile.LoadFromBase64(aFileName, aBase64: string);
Decoded := DecodeBase64(aBase64);
St := TBytesStream.Create(Decoded);
try
if not Assigned(FContent) then FContent := TMemoryStream.Create;
FContent.Clear;
St.Position := 0;
FContent.CopyFrom(St, 0);
Expand All @@ -798,6 +805,7 @@ procedure TAiMediaFile.LoadFromStream(aFileName: string; Stream: TMemoryStream);
begin
if Assigned(Stream) then
begin
if not Assigned(FContent) then FContent := TMemoryStream.Create;
FContent.Clear;
Stream.Position := 0;
FContent.CopyFrom(Stream, 0);
Expand All @@ -810,6 +818,7 @@ procedure TAiMediaFile.LoadFromStream(aFileName: string; Stream: TMemoryStream);

procedure TAiMediaFile.SaveToFile(aFileName: string);
begin
if not Assigned(FContent) then raise Exception.Create('TAiMediaFile.SaveToFile: no content loaded');
FContent.SaveToFile(aFileName);
end;

Expand All @@ -819,6 +828,7 @@ function TAiMediaFile.ToString: string;
begin
St := TStringStream.Create('');
try
if not Assigned(FContent) then FContent := TMemoryStream.Create;
FContent.Position := 0;
St.CopyFrom(FContent, 0);
Result := St.DataString;
Expand Down