📜 ⬆️ ⬇️

ISUII AmigaVirtual - universal AI for everyone

Logo Hello to all lovers and researchers of artificial intelligence! In this article, I would like to tell about an interesting project: the modular system of universal artificial intelligence (ISUAI) "Amiga Virtual" (AV, "Virtual Girlfriend"). I will talk about the basic principles of its work and describe some details of the implementation, and the most curious will be able to explore all the source codes. Development is conducted on Delphi, but the modules can theoretically be written in any PL. This system will be of interest both to end users of chat bots and related systems, and to developers of AI - after all, almost any type of AI can be developed on its basis.

First, a little history. The project was launched in 2012, when I decided to start developing a chat bot. And so I decided after becoming acquainted with “Sayme 2” - a simple flash game about communicating with a virtual girl (unfortunately, the ambitious continuation of this game - version 3 - apparently died in the bud. RIP, “Sayme 3”). Then AV was a monolith, with images in a single DLL with parts of an avatar - in order not to download extra megabytes if the avatar is not needed.

screenshot 1

screenshot 2

It looked like the first version of Amiga Virtual.

Since then, the concept has changed - now the AV itself - only the platform for organizing the work of the modules. All “organs” of AI are in modules.
')
A few words about the programming language. In 2012 I chose Delphi, because I didn’t know almost anything except him (not counting Turbo Pascal). Later I tried many languages ​​... Most of all I liked Ada, but there are few educational materials, and the programs are slow. I also tried Oxygen (Delphi Prism), but there are even less materials on it. C ++ and other C-like languages ​​I don’t like at all. Therefore, in the end, I returned to Delphi, which I know well and for which there is just a lot of educational material on the Internet, and there are also a lot of ready-made solutions.

Now about the project itself. Amiga Virtual is a multipurpose multi-agent system. In general terms, it is structured as follows: there is a main program (OP) and a set of modules (represented as DLLs and are software agents) that have certain interfaces from a certain list; during the start, the OP loads and identifies (determines what the module can do; the purpose of the module is not reported to the program — it does not make sense) all modules, then the user selects the modules with which he wants to work and starts multi-threaded processing of the input-output interfaces of the selected modules . This is how modules are loaded and identified:

procedure LoadModules; var i: Integer; begin for i := 0 to Length(Module) - 1 do begin if Module[i].Handle > 0 then FreeModule(Module[i]); if LoadModule(Module[i]) then DetermineModuleType(Module[i]); end; end; function LoadModule(var M: TModule): boolean; var B: String; C: Integer; begin with M do begin Handle := LoadLibrary(PWideChar(FileName)); if Handle > 0 then begin @SetLanguage := GetProcAddress(Handle, 'SetLanguage'); IncAndUpdateProgress; //  - @SendLanguageData := GetProcAddress(Handle, 'SendLanguageData'); IncAndUpdateProgress; @GetName := GetProcAddress(Handle, 'GetName'); IncAndUpdateProgress; //   ,   //      if @GetName <> nil then begin B := GetName; C := Pos(ControlCodeMarker, B); if C > 0 then begin Name := Copy(B, C + 1, Length(B)); ControlCode := Copy(B, 1, C - 1); ControlCodeFormated := FormatControlCode(ControlCode); end else begin Name := B; ControlCode := MainForm.LanguageData[133]; end; end else begin Name := MainForm.LanguageData[114]; ControlCode := MainForm.LanguageData[133]; end; if (@OpenWindow <> nil) and (@CloseWindow <> nil) then WindowState := closed else WindowState := window_does_not_exist; if (@Sleep <> nil) and (@WakeUp <> nil) then ModuleState := working else ModuleState := module_cant_sleep; LoadStatistics; //     Result := true; end else Result := false; end; end; function DetermineModuleType(var M: TModule): TModuleType; begin with M do begin MType := undetermined; if (@SetLanguage = nil) and ... (*     nil *) then MType := erroneous_or_empty else begin if (@SetSource <> nil) or (@NextData <> nil) or (@Progress <> nil) or (@RestartParsing <> nil) then MType := parser else begin if (@GetData <> nil) and (@SendData <> nil) then MType := input_and_output else if (@GetData <> nil) and (@SendData = nil) then MType := only_input else if (@GetData = nil) and (@SendData <> nil) then MType := only_output else if (@GetData = nil) and (@SendData = nil) then MType := no_input_and_no_output; end; end; Result := MType; end; end; 

OP implements only a text-based user interaction interface with modules, or, more simply, a chat window. All modules communicate through this chat, but usually the technical messages of the modules are not shown to the user, since They are of no interest to him (for debugging purposes, these messages can be displayed). Technical messages are marked with several non-printable characters at the beginning of a line. This code (“control code”) is defined by the developer of the module. Loading two modules with one control-code can lead to system looping - these two modules will endlessly exchange messages, like two mirrors set against each other. In short, the control code is needed so that the modules ignore messages that are not addressed to them. However, some modules can handle all messages - for example, modules of the intellect-core type.

Each module that has input, output, or input / output interfaces is processed in a separate OP thread. This allows you to maintain the performance of the OP, if a module hangs or just "thinks". The remaining modules are used by the user in manual mode through their own windows. The following class implements an I / O stream:

  TIOThread = class(TThread) public const SleepTime = 1000; var Module: TModule; SelfID: Integer; constructor Create(M: TModule; ID: Integer); protected procedure Execute; override; end; 

This is how I / O processing happens:

 procedure TIOThread.Execute; procedure GetData; var M: String; begin M := String(Module.GetData); if M <> SCM_No_Message then Synchronize( procedure begin Pool.AddRecord(M, SelfID); end); end; procedure SendData; var i: Integer; begin with Pool do if not Empty then begin for i := 0 to Length(Records) - 1 do with Records[i] do if not ModuleGot[SelfID] and (AuthorID <> SelfID) then begin Module.SendData(PChar(Text)); ModuleGot[SelfID] := true; end; Synchronize( procedure begin CheckAndDeleteOddRecords; end); end; end; begin inherited; while not Terminated do begin case Module.MType of only_input: GetData; only_output: SendData; input_and_output: begin SendData; GetData; end; end; Sleep(SleepTime); end; end; 

For messaging, the OP implements a message pool. It saves all messages that come from the chat window and from modules, and are stored until all modules receive these messages, then they are deleted. The following class implements a message pool:

  TPoolRecord = record Text: String; AuthorID: Integer; ModuleGot: array of Boolean; end; TPool = class Records: array of TPoolRecord; Empty: Boolean; procedure AddRecord(RecordText: String; RecordAuthor: Integer); procedure CheckAndDeleteOddRecords; constructor Create; procedure Show; end; procedure TPool.AddRecord(RecordText: String; RecordAuthor: Integer); var i, RL: Integer; begin RL := Length(Records); SetLength(Records, RL + 1); with Records[RL] do begin Text := RecordText; AuthorID := RecordAuthor; SetLength(ModuleGot, OutputModulesCount); for i := 0 to OutputModulesCount - 1 do if i = AuthorID then ModuleGot[i] := true else ModuleGot[i] := false; end; with MainForm, MainForm.ChatBox.Lines do case RecordAuthor of - 1: Add(User.Name + ': ' + RecordText); else if RecordText = SCM_Dont_Know_Answer then begin if DontKnowCheckBtn.Checked then Add(LanguageData[156]); end else Add(AVirtual.Name + ': ' + RecordText); end; Empty := false; end; procedure TPool.CheckAndDeleteOddRecords; function ItsOdd(ID: Integer): Boolean; var i: Integer; begin ItsOdd := true; with Records[ID] do for i := 0 to Length(ModuleGot) - 1 do if not ModuleGot[i] then begin ItsOdd := false; exit; end; end; procedure DeleteRecord(ID: Integer); var i: Integer; begin for i := ID to Length(Records) - 2 do Records[i] := Records[i + 1]; SetLength(Records, Length(Records) - 1); end; var i: Integer; begin if not Empty then begin for i := Length(Records) - 1 downto 0 do if ItsOdd(i) then DeleteRecord(i); if Length(Records) = 0 then Empty := true; end; if MainForm.PoolShowBtn.Checked then Show; end; 

Now let's talk about the possible classes of modules and their capabilities.

The most important, key class of modules for the sake of which the Girlfriend was conceived and implemented is the “intellect-cores” (FL, or just the cores). The kernel processes all messages arriving in the chat, in accordance with the algorithm embedded in it. That is, he performs the main intellectual work. The specific algorithm and its implementation depends on the kernel developer. How to make a good kernel is an interesting question, and I will consider it in another article on the example of my series of kernels. Another interesting question is how to split the kernel into separate modules. I can't subdivide the kernels I'm working on. But, in principle, nothing prevents you from making a composite core.

All other modules are divided into several categories: receptors (generate chat messages — module of vision, module of hearing, etc.), effectors (perform actions in response to messages from the chat), tools (not directly related to AI, are used by the user manually) ), parsers (used by the Dream Fusion training module), and other modules that can be used by some special modules.

Some receptors and effectors respectively encode and decode messages recorded in the "control code + command" style. Such messages are usually not displayed to the user. The essence of these messages is as follows: the receptor recognizes some action performed by the user, encodes the message and sends it to the core for storage. When the kernel issues this message, the corresponding effector decodes the message, receiving a command corresponding to the user's action, and performs this action. Since the core does not make any difference what messages to memorize and issue, and we can connect any receptors and effectors to the system — the intelligence is universal, that is, it can be taught anything. Great, right?

It should be replaced, for convenience, related receptors and effectors can be implemented in a single module. An example is a class of modules under the general name “emotional avatar” (I call this “emotar”): a user assembles a face from a part expressing a certain emotion, and the module creates a message encoding the selected emotion, i.e. acts as a receptor; when the kernel sends this message to the chat, the emotar decodes it and builds a face image with the corresponding emotion, i.e. acts as an effector. I do not see the point of separating the emotar into two separate modules

Here is an example of a template for creating a module in Delphi:

 library ; uses System.SysUtils, System.Classes, SystemControlMessagesUnit in '..\..\AmigaVirtual\SystemControlMessagesUnit.pas', MainFormUnit in 'MainFormUnit.pas', const ControlCode = ++; Name = ControlCode + '> '; Help = '' + #13 + ''; var FormState: (closed, opened); Buffer, VirtualName: String; NewMessageGot: Boolean; function GetName: PChar; stdcall; begin Result := PChar(Name); end; function GetHelp: PChar; stdcall; begin Result := PChar(Help); end; procedure OpenWindow; stdcall; begin if MainForm = nil then MainForm := TMainForm.Create(nil); MainForm.Show; FormState := opened; end; procedure CloseWindow; stdcall; begin if FormState = opened then begin MainForm.Close; FormState := closed; MainForm.Release; MainForm := nil; end; end; procedure SendData(Data: PChar); stdcall; begin Buffer := Data; end; function GetData: PChar; stdcall; begin if NewMessageGot then begin Result := PChar(Buffer); NewMessageGot := false; end else Result := PChar(SCM_No_Message); end; procedure Start; stdcall; begin NewMessageGot := false; if MainForm = nil then MainForm := TMainForm.Create(nil); end; procedure SetVirtual(Name: PChar); stdcall; begin VirtualName := Name; end; procedure LoadData; stdcall; begin //    end; procedure SaveData; stdcall; begin //    end; exports GetName, GetHelp, OpenWindow, CloseWindow, SendData, GetData, Start, SetVirtual, LoadData, SaveData; begin end. 

At the moment there are 21 interfaces:

  SetLanguage: function(Language: PChar): boolean; stdcall; SendLanguageData: function(Data: array of PChar): boolean; stdcall; GetName: function: PChar; stdcall; GetHelp: function: PChar; stdcall; Start: procedure; stdcall; Sleep: function: boolean; stdcall; WakeUp: procedure; stdcall; OpenWindow: procedure; stdcall; CloseWindow: procedure; stdcall; SetVirtual: procedure(Name: PChar); stdcall; SaveData: procedure; stdcall; LoadData: procedure; stdcall; Reset: procedure; stdcall; SetNewMainWindow: procedure(Position, Size: TPoint); stdcall; GetTimerInterval: function: Integer; stdcall; SendData: procedure(Data: PChar); stdcall; GetData: function: PChar; stdcall; SetSource: procedure(SourcePath: PChar); stdcall; NextData: function: PChar; stdcall; Progress: function: Real; stdcall; RestartParsing: procedure; stdcall; 

The module can implement any set of the above interfaces.

Is a typical module an intelligent agent? Depends on the implementation. I see two options for implementation. Consider them on the example of the module of view. The first option is a simple software agent: it encodes pictures as is, that is, converts bitmaps into a text string. The second option is a complex intelligent agent: for example, an artificial neural network that recognizes objects in pictures and describes them with words in a message. If several modules of the second type are used, then it can be said that the universal AI is built from a set of weak AI. Strong is intelligence or not - depends on the implementation of the kernel.

And now about the details of the main program. There are three of them: the organization of AI databases by name, the registration of users with the collection of statistics, and the “Exchange Center” for sharing content.

Know the chatbots platform iii? In fact, Amiga Virtual is much more advanced version iii. Only I call AI instances (named databases) not "Infi", but "Virtuals". Each Virtual is trained by the user from scratch and can be easily transferred to another computer. And with the help of avatar-modules (or emoters) a unique visual image of Virtual can be created. Tab Virtual Manager looks like this:

screenshot

The selection of the used modules is made with checking for collisions by the control code:

 type CResult = (no_collision, collision); function CheckModulesCollision: CResult; var i, j: Integer; CC: String; begin Result := no_collision; with ModulesList do for i := 0 to Items.Count - 1 do if Checked[i] then begin CC := FindModuleByFileName(ExtractDLLName(Items[i])).ControlCode; for j := 0 to Items.Count - 1 do if Checked[j] and (i <> j) and (CC = FindModuleByFileName(ExtractDLLName(Items[j])).ControlCode) then Result := collision; end; end; 

In order to know how AV is used by most users and in which direction to develop the project, I collect anonymous statistics.
While the collection of statistics is not implemented.

The Exchange Center (CO) is a service accessible from the main program, intended for the exchange of modules, Virtuals and other content between users:

screenshot

This is how content download is implemented:

 procedure TMainForm.DownloadFilesButtonClick(Sender: TObject); var Dir, FileName: String; begin SetCurrentDir(ProgramPath); Dir := Category[ContentCategoryBox.ItemIndex]; Dir := UpCase(Dir[1]) + Copy(Dir, 2, Length(Dir)); if not DirectoryExists(Dir) then CreateDir(Dir); SetCurrentDir(Dir); FileName := ContentList.Items[ContentList.ItemIndex]; if FileExists(FileName) then MessageDlg(LanguageData[172], mtInformation, [mbOk], 0) else begin DownloadFile(SiteProtocol + OfficialWebsite + ExchangeCenterPage + '?c=' + Category[ContentCategoryBox.ItemIndex] + '&f=' + FileName + '&l=' + LanguageData[0], FileName); //  LanguageData[0]     if Copy(FileName, Length(FileName) - 2, 3) = 'zip' then UnzipFiles(FileName, GetCurrentDir); case ContentCategoryBox.ItemIndex of 0: UpdateModulesList; 1: UpdateVirtualsList; end; SetStatusMessage(LanguageData[173] + ' ' + ProgramPath + Dir + '\'); end; end; procedure TMainForm.DownloadFile(From, SaveTo: String); var LoadStream: TMemoryStream; begin Downloading := true; LoadStream := TMemoryStream.Create; IdHTTP.Get(TIdURI.URLEncode(From), LoadStream); LoadStream.SaveToFile(SaveTo); LoadStream.Free; Downloading := false; SplashScreen.Close; end; 

The system of accounts is implemented for downloading content in the CO (it does not work yet):

screenshot

I plan to connect it with the accounts system of the official forum, but I don’t know how yet.

Basic access (download only) is provided in the guest account, and to upload your content you need to register (until the content download algorithm is implemented, I upload manually by FTP).

In addition to the CO, there is also a forum that provides technical support for users - it opens in the embedded browser (standard for Delphi - the Trident core seems to be used, but I don’t need much, just draw a simple page; the forum itself on phpBB):

screenshot

As you can see from the screenshot, you can enter your address and add the page to the bookmarks (the bookmarks have not yet been implemented) - in order not to lose the discussion on the forum.

One more thing. Noticed in the code above LanguageData? This is an array that stores text strings for GUI components that match the selected language. Russian and English are packaged in .exe as resources, and at start they are unpacked into the Languages ​​folder. Files with other languages ​​can be downloaded via the Sharing Center. Since Delphi supports Unicode, the language can be set to any one - Japanese or Arabic, for example.

This is how language files are unloaded from .exe:

 procedure TMainForm.DeployDefaultLanguages; procedure DeployLanguage(LanguageName: String); var ResHandle, MemHandle: THandle; MemStream: TMemoryStream; ResPtr: PByte; ResSize: Longint; ResName: String; i: Integer; begin ResName := ''; for i := 1 to Length(LanguageName) do ResName := ResName + UpCase(LanguageName[i]); ResName := ResName + '_LP'; ResHandle := FindResource(HInstance, PWideChar(ResName), RT_RCDATA); if ResHandle = 0 then begin ShowMessage('Default language "' + LanguageName + '" not found. (' + ResName + ')'); exit; end; MemHandle := LoadResource(HInstance, ResHandle); ResPtr := LockResource(MemHandle); MemStream := TMemoryStream.Create; ResSize := SizeOfResource(HInstance, ResHandle); MemStream.SetSize(ResSize); MemStream.Write(ResPtr^, ResSize); MemStream.Seek(0, 0); MemStream.SaveToFile(LangFilesDir + '/' + LanguageName + LanguageFileExtension); FreeResource(MemHandle); MemStream.Destroy; end; begin if not DirectoryExists(LangFilesDir) then CreateDir(LangFilesDir); DeployLanguage('Russian'); DeployLanguage('English'); end; 

As you can see, to embed a new pre-installed language, just paste the language file into the resource file, add a line of the DeployLanguage view ('NameLanguage') and recompile the project — conveniently.

This is how the language is loaded:

 procedure TMainForm.ChangeLanguageTo; function LanguageDataLoaded: Boolean; var T: TextFile; B: RawByteString; begin SetCurrentDir(ProgramPath + LangFilesDir); if FileExists(Language + LanguageFileExtension) then begin AssignFile(T, Language + LanguageFileExtension); Reset(T); SetLength(LanguageData, 1); LanguageData[0] := Language; while not eof(T) do begin SetLength(LanguageData, Length(LanguageData) + 1); ReadLn(T, B); LanguageData[Length(LanguageData) - 1] := UTF8ToWideString(B); end; CloseFile(T); if Length(LanguageData) - 1 >= LangFileMinSize then Result := true else Result := false; end else Result := false; end; procedure SetCaptions; begin with HelpForm do begin LoadHelpTexts; if CurrentTopic < HelpLast then OpenTopic(CurrentTopic) else OpenTopic(0); end; AddModulesHelpToMainProgramHelp; ChangeModulesLanguageToProgramLanguage; AuthorizationTab.Caption := LanguageData[18]; // - ... CloudSaveNow.Caption := LanguageData[181]; CloudLoadNow.Caption := LanguageData[182]; end; procedure CheckMenuItem; var Item: TMenuItem; begin for Item in LanguageMenu.Items do if Item.Name = LanguageData[0] + 'Lang' then Item.Checked := true; end; begin if LanguageDataLoaded then begin if not Silent then SetStatusMessage(LanguageData[2] + ' ' + LanguageData[0]) else FormCaption.Caption := LanguageData[1]; SetCaptions; end else begin if Language <> SavedLanguage then ChangeLanguageTo(SavedLanguage) else ChangeLanguageTo(DefaultLanguage); MessageDlg(LanguageData[3] + #13 + LanguageData[2] + ' ' + LanguageData[0] + '.', mtError, [mbOk], 0); end; CheckMenuItem; end; 

And there is also a window with a help:

screenshot

In it, through the main menu, you can display information about the module:

screenshot

Another interesting trick is the self-updating of the program, done using the .bat script:

 procedure TMainForm.UpdateProgram; procedure DeployBAT; var bat: TextFile; begin if not FileExists(ProgramPath + 'update.bat') then begin AssignFile(bat, ProgramPath + 'update.bat'); Rewrite(bat); WriteLn(bat, 'taskkill /im av.exe'); WriteLn(bat, 'sleep 1'); // Windows XP WriteLn(bat, 'timeout /t 1 /nobreak'); // Windows 7+ WriteLn(bat, 'del av.exe'); WriteLn(bat, 'move ' + ZipsDir + '\av.exe %1'); WriteLn(bat, 'del /S /Q ' + ZipsDir); WriteLn(bat, 'start av.exe'); // WriteLn(bat, 'pause'); CloseFile(bat); end; end; begin DownloadFile(SiteProtocol + OfficialWebsite + '/av.zip', ProgramPath + 'av.zip'); UnzipFiles(ProgramPath + 'av.zip', ProgramPath + ZipsDir); DeployBAT; SetCurrentDir(ProgramPath); ShellExecute(Handle, nil, 'update.bat', PChar(ProgramPath), nil, SW_SHOW); end; 

Finally, I want to note that the Amiga Virtual project includes not only a Windows program. In addition to it, system options are planned for Android (AV Mobile), robotic platforms (AV OS) and supercomputers (AV Super). And algorithms of intellect-cores can be used to create an intelligent search engine (reorganizing Yandex and Google search results). When the alpha version is ready in one of these areas, I will write an article describing her work.

Project source codes and design document can be downloaded here: github.com/TimKruz/AV

Thanks for attention.

Source: https://habr.com/ru/post/316830/


All Articles