Notice: Undefined index: print in /home/ftpsite/joshis.iprofil.cz/articles.php on line 61
Programování inteligentních agentů - 3. díl - joshis.iprofil.cz
 
[ Webhosting profitux.cz ]
Nové osobní stránky Petra Dvořáka

Moje články

hertpl: Undefined index: page_title(template line: 251) in templates/articles.html on line 145

Verze pro tisk (ladí se...) >>

Programování inteligentních agentů - 3. díl

Úvod

Minule jsme si napsali první - leč ne zcela použitelný - program v jazyce 3APL. Dnes si napíšeme o něco komplexnějšího agenta - "sběrače bomb". Seznámíme se přitom s prostředím BlockWorld (prostředí vestavěné do platformy 3APL) a povíme si něco o rozlišení "achievement" a "maintanance" cílů agenta a o tom, jak k tomuto rozdělení přistupuje 3APL.

BlockWorld - základní prostředí

Platforma 3APL obsahuje vestavěné prostředí BlockWorld. Dokumentace prostředí je uvedená i v dokumentaci 3APL, my si jej zde přesto stručně popíšeme. BlockWorld je vlastně čtvercová síť nastavitelných rozměrů, která může obsahovat libovolný počet stěn, bomb a také právě jednu "past" (místo, kde mizí bomby, pokud je tam agent položí). V této síti se mohou pohybovat jednotliví agenti (jedná se tedy potenciálně o sdílené prostředí) - těm je zároveň možno nastavit, jak daleko dohlédnou ("sense-range"). Pokyny, které posílá platforma 3APL prostředí BlockWoldu jsou vlastně akcemi agenta v prostředí. Mají tento obecný tvar:

Java("BlockWorld", Akce(), Navratova_hodnota)

Jendotlivé akce přitom mohou být tyto:

  • void enter(X, Y, C) - Umístí agenta do BlockWorldu na pozici X, Y s časovým zpožděním C milisekund (-1 = nekonečno).
  • boolean north () - Posune agenta o pole nahorů.
  • boolean east () - Posune agenta o pole vpravo.
  • boolean south () - Posune agenta o pole dolů.
  • boolean west () - Posune agenta o pole vlevo.
  • boolean pickup () - Pokusí se vzít bombu na aktuální pozici agenta.
  • boolean drop () - Pokusí se položit bombu na aktuální pozici agenta.
  • ListPar senseTrap () - Zjistí polohu "pasti".
  • ListPar sensePosition () - Zjistí svoji pozici.
  • ListPar senseBombs () - Zjisti polohu bomb v rámci sense-range.
  • ListPar senseStones () - Zjisti polohu stěn v rámci sense-range.
  • ListPar senseAgents () - Zjisti polohu agentů v rámci sense-range.

Typ ListPar znamená Prologovský seznam dvojic (jedné či více pozic v dvojrozměrném světě), boolean je tradiční - TRUE/FALSE.

Detailněji používání BlockWorldu uvidíme na následujícím příkladě.

Agent "chytrý sběrač bomb"

Zadání úlohy, kterou teď budeme řešit, je následující. Máme svět (BlockWorld), ve kterém se nachází bomby a jedna past, která umí tyto bomby zničit. Chceme napsat agenta, který prochází prostředím "po krocích" (neumí se teleportovat o více než jeden blok) a tyto bomby vyhledává, sbírá a následně zneškodňuje přenesením do pasti. Zároveň chceme, aby agent nemusel hned všechny bomby vidět, čili aby si vystačil i s omezeným rozhledem. Taky chceme, aby byl agent v některých činnostech poněkud chytřejší - tomu se budeme věnovat u jednotlivých částí kódu.

Začněme tedy jako obvykle:

// Soubor exc_02.3apl
PROGRAM "logistic-agent"

Nyní si připravíme pomocný Prolog soubor, který si následně načteme v našem 3APL programu. Tento soubor bude obsahovat některé důležité predikáty. Konkrétně predikáty pro zjištění, zdali se v seznamu souřadnic nachází bomba/past na daných souřadnicích, dále potom predikát na generování souřadnic náhodné pozice v případě, že agent nezná souřadnici žádné bomby a neví, kam jít.

%soubor exc_02.pl
 
bombAt(X,Y,[[X,Y]|T]).
bombAt(X,Y,[[X1,Y1]|T]):-bombAt(X,Y,T).
 
trapAt(X,Y,[X,Y]).
 
new_target(X,Y, DimX, DimY) :-
  random(0,DimX,X),
  random(0,DimY,Y).
random(From, To, Rand) :-
  Rand is int(From + random(To - From)).

Další řádek načte tento Prolog soubor:

LOAD "exc_02.pl"

Nyní přistoupíme k definici schopností agenta. Bude to podstatně komplexnější, nežli minule:

CAPABILITIES {
  {pos(U, V) AND (NOT U=X OR NOT V=Y)} MoveTo(X, Y)            {not pos(U, V), pos(X, Y)}
  {target(X, Y) AND pos(X, Y)}         Realize_InPosition()    {NOT target(X, Y)}
  {NOT target(U, V)}                   ChooseDestination(X, Y) {target(X, Y)}
  {target(U, V)}                       ChooseDestination(X, Y) {NOT target(U, V), target(X, Y)}
  {true}                               RealizeBomb(X, Y)       {bomb(X, Y)}
  {hasBomb() AND lastBomb(X,Y)}        DropTheBomb()           {NOT hasBomb(), NOT lastBomb(X,Y)}
  {pos(X, Y) AND bomb(X, Y)}           PickUpTheBomb()         {hasBomb(), target(X, Y), NOT bomb(X, Y), lastBomb(X,Y)}
  {lastBomb(X,Y) AND trap(TX,TY)}      NoBombRound()           {NOT lastBomb(X,Y), lastBomb(TX,TY)}
  {bomb(X,Y)}                          BombDissappeared(X,Y)   {NOT bomb(X,Y)}
  {NOT trap(U, V)}                     RememberTrap(X, Y)      {trap(X, Y), findTrap()}
  {trap(U, V)}                         RememberTrap(X, Y)      {NOT trap(U, V), trap(X, Y), findTrap()}
}

Probereme si postupně jednotlivé akce, které je agent schopný provést. První akce - MoveTo(X,Y) - přesune agenta na pozici [X,Y], pokud tam ještě není. Druhá akce - Realize_InPosi­tion() - slouží agentovi k uvědomnění toho, že je již na pozici, kterou si dříve zvolil za cíl. Třetí a čtvrtý řádek - ChoseDestinati­on(X,Y) - slouží právě k volbě cíle, resp. k jeho revizi. Pátý řádek - akce RealizeBomb(X,Y) - způsobí, že si agent do "belief" báze uloží, že na [X,Y] je bomba. Šestý řádek - DropTheBomb() - specifikuje, jak agent může položit bombu. Sedmý (PickUpTheBomb()) naopak definuje, jak agent bombu sebere. Osmý řádek s akcí NoBombRound() definuje změny stavu agenta v případě, že agent nevidí okolo sebe žádnou bombu. Devátý řádek s akcí BombDissappea­red(X,Y) umožňuje agentovi vyrovnat se s tím, že z prostředí zmizí bomba, kterou už jednou viděl na pozici [X,Y] a uložil si ji do své "belief" báze. Poslední dva řádky - RememberTrap(X,Y) - slouží k zapamatování si pozice pasti na začátku agentovi činnosti. Opět přípomínám, že jsme nedefinovali (!!!) žádná pravidla uvažování. Pouze jsme řekli, jaké akce agent umí vykonat, kdy je agent smí provést a jaké ty dané akce mají následky.

Poslední odstavec byl relativně složitý, proto trochu odlehčíme. Definujeme si napřed počáteční "belief" bázi agenta. Ta bude obsahovat informace o počáteční pozici agenta a v rámci čistoty kódu také informaci o velikosti světa (tím se vyhneme potřebě na více místech přepisovat kód). Dále si definujeme úvodní cíl agenta - najít pozici pasti, do které bude následně házet bomby. Poté, co se agentovi tento cíl splní, adoptuje si nový cíl - hledání bomb. Do plánů agenta přidáme kód pro umístění agenta do BlockWorldu (podívejte se na to - je to poprvé, co v programu BlockWorld zmiňujeme).

BELIEFBASE {
  pos(0,0).
  worldsize(15,15).
}
 
GOALBASE {
  findTrap()
}
 
PLANBASE {
  Java("BlockWorld", enter(0,0,-1), L),
}

Přistupme nyní k definici pravidel pro plánování cílů. Tento kus kódu bude asi to nejkomplexnější, s čím se dnes setkáme. Význam jednotlivých pravidel jsem se rozhodl - v rámci přehlednosti - uvést v komentářích přímo před jednotlivými částmi kódu.

Začněme tedy:

PG-RULES {

Provní pravidlo říká, že pokud dorazí agent na misto, které si předtím zvolil, a není na něm bomba, rozhlédne se. Pokud vidí bombu, jde hned po ní.

<- target(X, Y) AND pos(X, Y) AND NOT bomb(X,Y) |
            {
              Realize_InPosition();
              Java("BlockWorld", senseBombs(), BOMBS);
              IF bombAt(Xi, Yi, BOMBS) THEN {
                ChooseDestination(Xi, Yi);
              }
            }

Druhé pravidlo definuje chování chování agenta v případě, že mu neočekávaně zmizí z prostředí bomba: Pokud je agent na pozici, kterou si adoptoval za cíl, a je na ní podle jeho "belief" báze bomba, zjistí, zdali tam ta bomba opravdu je. Pokud ne, updatuje si belief bázi, resp. odstraní z ní zmizlou bombu.

<- target(X, Y) AND pos(X, Y) AND bomb(X,Y) |
            {
              Realize_InPosition();
              Java("BlockWorld", senseBombs(), BOMBS);
              IF NOT bombAt(X, Y, BOMBS) THEN {
                BombDissappeared(X,Y);
              }
            }

Následuje 4x pravidlo pro pohyb do 4 směrů, při každém kroku vnímá agent okolní bomby a ukládá si je do "belief" baze.

<- target(X, Y) AND pos(U, V) AND U < X | { MoveTo(U + 1,V); Java("BlockWorld", east(), L); Java("BlockWorld", senseBombs(), BOMBS); IF bombAt(Xi, Yi, BOMBS) THEN { RealizeBomb(Xi, Yi) } }
<- target(X, Y) AND pos(U, V) AND U > X | { MoveTo(U - 1,V); Java("BlockWorld", west(), L); Java("BlockWorld", senseBombs(), BOMBS); IF bombAt(Xi, Yi, BOMBS) THEN { RealizeBomb(Xi, Yi) }}
<- target(X, Y) AND pos(U, V) AND V < Y | { MoveTo(U,V + 1); Java("BlockWorld", south(), L); Java("BlockWorld", senseBombs(), BOMBS); IF bombAt(Xi, Yi, BOMBS) THEN { RealizeBomb(Xi, Yi) }}
<- target(X, Y) AND pos(U, V) AND V > Y | { MoveTo(U,V - 1); Java("BlockWorld", north(), L); Java("BlockWorld", senseBombs(), BOMBS); IF bombAt(Xi, Yi, BOMBS) THEN { RealizeBomb(Xi, Yi) }}

Následuje pravidlo pro hledání pozice pasti. Pravidlo agetovi říká: "Rozhlédni se. Když nevidíš past, zvol náhodný cíl, a jdi se rozhlížet tam. Pokud past vidíš, ulož si její polohu a adoptuj si nový cil - hledání bomb".

findTrap() <- NOT trap(XT,YT) AND NOT target(Xa,Ya) AND worldsize(WSX, WSY) |
            {
              Java("BlockWorld", senseTrap(), TRAP);
              IF trapAt(A, B, TRAP) THEN {
                RememberTrap(A,B);
                AdoptGoal(findBomb())
              } ELSE {
                new_target(X0, Y0, WSX, WSY)?;
                ChooseDestination(X0,Y0);
              }
            }

Další pravidlo zajistí, že pokud je někde nahromaděno více bomb, agent tomu přizpůsobí své chování a vysbírá všechny takto nakupené bomby. Říká doslova: "Nemáš-li bombu, víš-li kde je past a stojíš-li na bombě, seber bombu (tu na které stojíš) a zjisti, zda jsou v okolí další bomby. Pokud ano, ulož si svou pozici pro návrat (toto je chování uložené implicitně v PickUpTheBomb, see CAPABILITIES). Jinak si nic neukládej (NoBombRound())." Zde poznamenejme, že slovy "jinak si nic nic neukládej" je myšleno "ulož si pozici poslední bomby jako pozici pasti" (see CAPABILITIES). Každopadně se agent vrátí na pozici pasti, čili [TX,TY], aby tam mohl bombu, kterou sebral zneškodnit..

findBomb() <- trap(TX, TY) AND pos(X, Y) AND bomb(X, Y) AND NOT hasBomb() |
            {
              PickUpTheBomb();
              Java("BlockWorld", pickup(), L);
              Java("BlockWorld", senseBombs(), BOMBS);
              IF NOT bombAt(Xi, Yi, BOMBS) THEN {
                NoBombRound();
                ChooseDestination(TX, TY);
              } ELSE {
                ChooseDestination(TX, TY);
              }
            }

Dále: pokud je agent na pasti a má bombu, položí (a zneškodní tak) bombu, vnímá, jsou-li bomby někde okolo něj. Když vidí bombu, jde ji sebrat, jinak se vrátí na pozici poslední bomby, pokud ji má uloženou (zde muže být jako pozice poslední bomby uložená pozice pasti - v takovém případě se agent pohybuje buď ke známé bombě, nebo náhodně pokud o žádné bombě neví, see CAPABILITIES).

findBomb() <- trap(X, Y) AND pos(X, Y) AND hasBomb() AND lastBomb(LBX, LBY) |
            {
              DropTheBomb();
              Java("BlockWorld", drop(), L);
              Java("BlockWorld", senseBombs(), BOMBS);
              IF bombAt(Xi, Yi, BOMBS) THEN {
                RealizeBomb(Xi, Yi);
                ChooseDestination(Xi, Yi);
              } ELSE {
                ChooseDestination(LBX,LBY);
              }
            }

Pravidlo uvedené dále specifikuje chování agenta v případě, že ví o nějaké bombě - pokud agent nemá kam jít, nemá bombu, ale ví o nějaké bombě, zvolí si za cíl polohu té bomby.

findBomb() <- NOT target(X,Y) AND pos(U, V) AND bomb(X,Y) AND (NOT X=U OR NOT Y=V) AND NOT hasBomb() |
            {
              ChooseDestination(X, Y);
            }

Pokud je agent na místě, kde není bomba a nemá bombu, zjistí, je-li někde kolem něj bomba. Pokud ano, sebere ji, pokud ne, vybere si náhodné místo kam jít a vydá se tam zkusit štěstí. Toto pravidlo de facto zajišťuje náhodné procházení prostoru agenta BlockWorldem v případě, že agent nevidí žádnou bombu a ani o žádné bombě neví.

findBomb() <- pos(X, Y) AND NOT bomb(X,Y) AND NOT hasBomb() AND worldsize(WSX, WSY) |
            {
              Java("BlockWorld", senseBombs(), BOMBS);
              IF bombAt(Xi, Yi, BOMBS) THEN {
                RealizeBomb(Xi, Yi);
                ChooseDestination(Xi, Yi);
              } ELSE {
                new_target(Xj, Yj, WSX, WSY)?;
                ChooseDestination(Xj,Yj);
              }
            }
}

Klauzule PR-RULES je v tomto příkladu opět prázdná.

PR-RULES {}

Celý program (bez komentářů) vypadá tedy takto:

PROGRAM "logistic-agent"
LOAD "exc_02.pl"
 
CAPABILITIES {
  {pos(U, V) AND (NOT U=X OR NOT V=Y)} MoveTo(X, Y)            {not pos(U, V), pos(X, Y)}
  {target(X, Y) AND pos(X, Y)}         Realize_InPosition()    {NOT target(X, Y)}
  {NOT target(U, V)}                   ChooseDestination(X, Y) {target(X, Y)}
  {target(U, V)}                       ChooseDestination(X, Y) {NOT target(U, V), target(X, Y)}
  {true}                               RealizeBomb(X, Y)       {bomb(X, Y)}
  {hasBomb() AND lastBomb(X,Y)}        DropTheBomb()           {NOT hasBomb(), NOT lastBomb(X,Y)}
  {pos(X, Y) AND bomb(X, Y)}           PickUpTheBomb()         {hasBomb(), target(X, Y), NOT bomb(X, Y), lastBomb(X,Y)}
  {lastBomb(X,Y) AND trap(TX,TY)}      NoBombRound()           {NOT lastBomb(X,Y), lastBomb(TX,TY)}
  {bomb(X,Y)}                          BombDissappeared(X,Y)   {NOT bomb(X,Y)}
  {NOT trap(U, V)}                     RememberTrap(X, Y)      {trap(X, Y), findTrap()}
  {trap(U, V)}                         RememberTrap(X, Y)      {NOT trap(U, V), trap(X, Y), findTrap()}
}
 
BELIEFBASE {
  pos(0,0). worldsize(15,15).
}
 
GOALBASE {
  findTrap()
}
 
PLANBASE {
  Java("BlockWorld", enter(0,0,-1), L),
}
 
PG-RULES {
  <- target(X, Y) AND pos(X, Y) AND NOT bomb(X,Y) |
              {
                Realize_InPosition();
                Java("BlockWorld", senseBombs(), BOMBS);
                IF bombAt(Xi, Yi, BOMBS) THEN {
                  ChooseDestination(Xi, Yi);
                }
              }
 
  <- target(X, Y) AND pos(X, Y) AND bomb(X,Y) |
              {
                Realize_InPosition();
                Java("BlockWorld", senseBombs(), BOMBS);
                IF NOT bombAt(X, Y, BOMBS) THEN {
                  BombDissappeared(X,Y);
                }
              }
 
  <- target(X, Y) AND pos(U, V) AND U < X | { MoveTo(U + 1,V); Java("BlockWorld", east(), L); Java("BlockWorld", senseBombs(), BOMBS); IF bombAt(Xi, Yi, BOMBS) THEN { RealizeBomb(Xi, Yi) } }
  <- target(X, Y) AND pos(U, V) AND U > X | { MoveTo(U - 1,V); Java("BlockWorld", west(), L); Java("BlockWorld", senseBombs(), BOMBS); IF bombAt(Xi, Yi, BOMBS) THEN { RealizeBomb(Xi, Yi) }}
  <- target(X, Y) AND pos(U, V) AND V < Y | { MoveTo(U,V + 1); Java("BlockWorld", south(), L); Java("BlockWorld", senseBombs(), BOMBS); IF bombAt(Xi, Yi, BOMBS) THEN { RealizeBomb(Xi, Yi) }}
  <- target(X, Y) AND pos(U, V) AND V > Y | { MoveTo(U,V - 1); Java("BlockWorld", north(), L); Java("BlockWorld", senseBombs(), BOMBS); IF bombAt(Xi, Yi, BOMBS) THEN { RealizeBomb(Xi, Yi) }}
 
  findTrap() <- NOT trap(XT,YT) AND NOT target(Xa,Ya) AND worldsize(WSX, WSY) |
              {
                Java("BlockWorld", senseTrap(), TRAP);
                IF trapAt(A, B, TRAP) THEN {
                  RememberTrap(A,B);
                  AdoptGoal(findBomb())
                } ELSE {
                  new_target(X0, Y0, WSX, WSY)?;
                  ChooseDestination(X0,Y0);
                }
              }
 
  findBomb() <- trap(TX, TY) AND pos(X, Y) AND bomb(X, Y) AND NOT hasBomb() |
              {
                PickUpTheBomb();
                Java("BlockWorld", pickup(), L);
                Java("BlockWorld", senseBombs(), BOMBS);
                IF NOT bombAt(Xi, Yi, BOMBS) THEN {
                  NoBombRound();
                  ChooseDestination(TX, TY);
                } ELSE {
                  ChooseDestination(TX, TY);
                }
              }
 
  findBomb() <- trap(X, Y) AND pos(X, Y) AND hasBomb() AND lastBomb(LBX, LBY) |
              {
                DropTheBomb();
                Java("BlockWorld", drop(), L);
                Java("BlockWorld", senseBombs(), BOMBS);
                IF bombAt(Xi, Yi, BOMBS) THEN {
                  RealizeBomb(Xi, Yi);
                  ChooseDestination(Xi, Yi);
                } ELSE {
                  ChooseDestination(LBX,LBY);
                }
              }
 
  findBomb() <- NOT target(X,Y) AND pos(U, V) AND bomb(X,Y) AND (NOT X=U OR NOT Y=V) AND NOT hasBomb() |
              {
                ChooseDestination(X, Y);
              }
 
  findBomb() <- pos(X, Y) AND NOT bomb(X,Y) AND NOT hasBomb() AND worldsize(WSX, WSY) |
              {
                Java("BlockWorld", senseBombs(), BOMBS);
                IF bombAt(Xi, Yi, BOMBS) THEN {
                  RealizeBomb(Xi, Yi);
                  ChooseDestination(Xi, Yi);
                } ELSE {
                  new_target(Xj, Yj, WSX, WSY)?;
                  ChooseDestination(Xj,Yj);
                }
              }
}
 
PR-RULES {
}

Abychom ukázali, že výsledné chování takto definovaného agenta je poměrně dobré, ukážeme si zde malou videoukázku.

Náš agent "sběrač bomb" se ve výsledku může chovat třeba takto:

Get Flash to see this player.

Srovnání: "achievement" vs. "maintanance" cíle

V úvodu článku jsem mimojiné sliboval, že si rozebereme rozdíl mezi "achievement" a "maintanance" cíli - teď je na to asi ten pravý čas. Budeme přitom používat tyto původní, z angličtiny převzaté pojmy, neboť počeštěně znějí poněkud podivně (to achieve = dosáhnout, to maintain = udržovat).

Pokud se chceme striktně držet definice Cohena a Levesqua [Coh&Lev, 3.6.1], dojdeme k následujícímu:

Jako "achievement" cíl chápeme takový cíl, který jsme ještě nedosáhli a v budoucnu jej chceme dosáhnout, respektive je to takový cíl, který nám přinese něco nového - tedy formálně dle Cohena a Levesqua:

A-GOAL(p) :=def ¬BELIEVE(p) & GOAL(LATER(p))
LATER(p) :=def ¬p & ◊p

Mohli bychom zde uvést pro příklad cíle "zbohatnout", "koupit si nový automobil", "jet na dovolenou",...

Naproti tomu "maintenance" cíl je takový, který už agent někdy dosáhl (tedy oproti výše uvedenému v něj již věří), a u kterého je vlastně potřeba vykonávat akce, které - nepřesně a ne příliš konkrétně řečeno - nezneplatní daný cíl. Například pokud nechceme být chudí (neboli máme "maintenance" cíl nebýt chudí), nesmíme podniknout akce, které by nás o peníze připravily (třeba hrát na hracích automatech nebo koupit majoritní podíl Applu).

Co je teď ale zajímavé je to, jakým způsobem k maintanance cílům přistupuje 3APL. Pokud totiž agent běžící na této platformě má za cíl A, a pokud v nějakém okamžiku věří, že A platí, okamžitě cíl A upustí a A tedy již není jeho cílem. To není to, co bychom podle výše uvedené definice vždy chtěli (resp. 3APL si s maintenance cíli ve výše uvedeném pojetí neumí poradit a de facto pracuje jen s achievement góly).

Jak autoři platformy 3APL sami tvrdí, je to proto, že oni chápou "maintanance" cíle poněkud odlišně. Jako příklad mi uvedli situaci, kdy chceme mít v nádrži od auta dostatek benzínu. To je totiž vlastně také "maintanance" cíl. Když jednou doplníme nádrž, tento cíl můžeme dočasně upustit - není potřeba neustále kontrolovat, jestli je nádrž ze 100% plná. Stačí nám vlastně počkat na jakýsi "alarm" (v našem případě jej představuje oranžová kontrolka), který mi řekne, že je v nádrži nedostatek benzínu (a tudíž ohrožen cíl). Potom je tedy nutné znovu začít podnikat akce k tomu, aby se doplnila nádrž (zastavit u benzínky nebo si "půjčit" od souseda).

Maintenance cíl tedy podle nich není striktně jen cíl, ve který agent věří, ale spíše takový, který - když hrozí jeho ztráta - spustí jakýsi alarm, který agenta donutí znovu si cíl adoptovat (k adoptování cíle v 3APL slouží příkaz AdoptGoal(neja­ky_cil())). Nebo i cíl, ve který agent nebude nikdy věřit (jako je to např. s cílem findBomb() z našeho příkladu) a který agenta nutí vykonávat nějakou činnost (například udržovat svět bez bomb).


Doufám, že se Vám dnešní díl líbil. V příštím pokračování se podíváme na to, jak se dají v 3APL vytvářet multi-agentní systémy.

Související články:


Nový komentář k článku "Programování inteligentních agentů - 3. díl"

Podpis (smí obsahovat url ve tvaru "http://domeny")
Zde napište slovo "člověk" (malá písmena, smí být bez diakritiky)
Text příspěvku (Texy markup)

Komentáře čtenářů

hertpl: Undefined index: comments(template line: 478) in templates/articles.html on line 219