diff --git a/lib/ResolveActions.function.php b/lib/ResolveActions.function.php
index 8481780..63e1d5f 100644
--- a/lib/ResolveActions.function.php
+++ b/lib/ResolveActions.function.php
@@ -1,45 +1,50 @@
function_name = $s; return $this; }
+ public function setArgumentsTypes(array $a) { $this->args_type = $a; $this->no_of_args = count($a); return $this; }
+ public function getExpectedNoOfArguments() { return $this->no_of_args; }
+ public function getFunctionName() { return $this->function_name; }
+}
+function registerAction(Action $a) { global $actions; $actions[$a->getFunctionName()] = $a; return true;}
+// Register all actions available
+require_once __DIR__."/actions/index.php";
+
function ResolveActions(string $text) {
+ global $actions;
+
+ $function_name_regex_format = "@([a-zA-Z_][a-zA-Z0-9_]+)";
+ $arguments_regex_format = "([\w:\/\.-]+)";
- if (preg_match("/@rss_reader ([\w:\/\.]+)/",$text,$matches)) {
- $url = $matches[1];
+ if (preg_match("/$function_name_regex_format( |$)/",$text,$matches)) {
+ $function_name = $matches[1];
- // Initialize cURL
- $ch = curl_init();
- curl_setopt_array($ch, [
- CURLOPT_URL => $url,
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_FOLLOWLOCATION => true,
- CURLOPT_TIMEOUT => 10,
- CURLOPT_USERAGENT => "PHP cURL RSS Reader"
- ]);
-
- $response = curl_exec($ch);
-
- if (curl_errno($ch)) die("cURL error: " . curl_error($ch));
- curl_close($ch);
-
- // Parse as XML
- $xml = @simplexml_load_string($response);
-
- if (!$xml) die("Failed to parse XML.");
-
- // Find first
- $firstTitle = '';
- if (isset($xml->channel->item[0]->title)) {
- // RSS 2.0 style
- $firstTitle = (string)$xml->channel->item[0]->title;
- } elseif (isset($xml->entry[0]->title)) {
- // Atom style
- $firstTitle = (string)$xml->entry[0]->title;
- } elseif (isset($xml->title)) {
- // fallback
- $firstTitle = (string)$xml->title;
- }
-
+ if ( !isset($actions[$function_name]) ||
+ !function_exists($function_name )) return "[[ Action $function_name not available. ]]";
- $text = str_replace($matches[0],$firstTitle,$text);
+ $action = $actions[$function_name];
+
+ // Fetch the arguments
+ $reg_exp = array_fill(0,$action->getExpectedNoOfArguments(),"([\w:\/\.-]+)");
+ array_unshift($reg_exp, $function_name_regex_format);
+
+ if (!preg_match("/".implode(" ",$reg_exp)."/",$text,$matches))
+ return "[[ Action $function_name was passed with wrong number of arguemnts. Expected: ".$a->getExpectedNoOfArguments()." ]]";
+
+ $full_action_requested_string = $matches[0];
+
+ array_shift($matches); // Clip the first whole-string result
+ array_shift($matches); // Clip the function name
+ $arguments = $matches;
+ $actionResult = call_user_func_array($function_name, $arguments);
+
+ $text = str_replace($full_action_requested_string,$actionResult,$text);
}
return $text;
diff --git a/lib/actions/action.sample.php b/lib/actions/action.sample.php
new file mode 100644
index 0000000..9605258
--- /dev/null
+++ b/lib/actions/action.sample.php
@@ -0,0 +1,30 @@
+setFunctionName("sayHello")
+ ->setArgumentsTypes(["name", "age"]) // name, age
+);
+
+/**
+ * 2. Implement the function
+ *
+ * @param string $name
+ * @param int $age
+ * @return string
+ */
+function sayHello(string $name, int $age): string {
+ return "Hello $name, you are $age years old!";
+}
diff --git a/lib/actions/index.php b/lib/actions/index.php
new file mode 100644
index 0000000..e02f7fe
--- /dev/null
+++ b/lib/actions/index.php
@@ -0,0 +1,6 @@
+setFunctionName("getFirstTitleFromRSSFeed")->setArgumentsTypes(["url"]));
+
+function getFirstTitleFromRSSFeed(string $url) {
+ // Initialize cURL
+ $ch = curl_init();
+ curl_setopt_array($ch, [
+ CURLOPT_URL => $url,
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_TIMEOUT => 10,
+ CURLOPT_USERAGENT => "PHP cURL RSS Reader"
+ ]);
+
+ $response = curl_exec($ch);
+
+ if (curl_errno($ch)) die("cURL error: " . curl_error($ch));
+ curl_close($ch);
+
+ // Parse as XML
+ $xml = @simplexml_load_string($response);
+
+ if (!$xml) die("Failed to parse XML.");
+
+ // Find first
+ $firstTitle = '';
+ if (isset($xml->channel->item[0]->title)) {
+ // RSS 2.0 style
+ $firstTitle = (string)$xml->channel->item[0]->title;
+ } elseif (isset($xml->entry[0]->title)) {
+ // Atom style
+ $firstTitle = (string)$xml->entry[0]->title;
+ } elseif (isset($xml->title)) {
+ // fallback
+ $firstTitle = (string)$xml->title;
+ }
+
+ return $firstTitle;
+}
diff --git a/lib/actions/weather.action.php b/lib/actions/weather.action.php
new file mode 100644
index 0000000..09fcbf6
--- /dev/null
+++ b/lib/actions/weather.action.php
@@ -0,0 +1,303 @@
+setFunctionName("weather_today")
+ ->setArgumentsTypes(["string"]) // location
+);
+
+registerAction(
+ (new Action())
+ ->setFunctionName("weather_tomorrow")
+ ->setArgumentsTypes(["string"]) // location
+);
+
+registerAction(
+ (new Action())
+ ->setFunctionName("weather_this_week")
+ ->setArgumentsTypes(["string"]) // location
+);
+
+// ───────────────────────────────────────────────────────────────────────────────
+// 2) Public action functions
+// ───────────────────────────────────────────────────────────────────────────────
+
+/**
+ * @param string $place e.g. "Lisbon"
+ * @return string
+ */
+function weather_today(string $place): string {
+ $ctx = wm_build_context($place);
+ if (isset($ctx['error'])) return $ctx['error'];
+
+ // index 0 = today
+ $i = 0;
+ $d = $ctx['daily'];
+ if (!isset($d['time'][$i])) return "No forecast found for today in {$ctx['name']}.";
+
+ $dateLabel = wm_pretty_date($d['time'][$i], $ctx['tz']);
+ return wm_format_day_summary("Today in {$ctx['name']}", $dateLabel, $d, $i);
+}
+
+/**
+ * @param string $place
+ * @return string
+ */
+function weather_tomorrow(string $place): string {
+ $ctx = wm_build_context($place);
+ if (isset($ctx['error'])) return $ctx['error'];
+
+ // index 1 = tomorrow
+ $i = 1;
+ $d = $ctx['daily'];
+ if (!isset($d['time'][$i])) return "No forecast found for tomorrow in {$ctx['name']}.";
+
+ $dateLabel = wm_pretty_date($d['time'][$i], $ctx['tz']);
+ return wm_format_day_summary("Tomorrow in {$ctx['name']}", $dateLabel, $d, $i);
+}
+
+/**
+ * @param string $place
+ * @return string
+ */
+function weather_this_week(string $place): string {
+ $ctx = wm_build_context($place);
+ if (isset($ctx['error'])) return $ctx['error'];
+
+ $d = $ctx['daily'];
+ if (empty($d['time'])) return "No weekly forecast found for {$ctx['name']}.";
+
+ $lines = [];
+ $lines[] = "This week in {$ctx['name']} ({$ctx['tz']}):";
+
+ $n = min(count($d['time']), 7);
+ for ($i = 0; $i < $n; $i++) {
+ $date = $d['time'][$i];
+ $label = wm_pretty_weekday($date, $ctx['tz']); // e.g., Mon 2025-09-15
+ $lines[] = "• " . wm_format_compact_line($label, $d, $i);
+ }
+
+ return implode("\n", $lines);
+}
+
+// ───────────────────────────────────────────────────────────────────────────────
+// 3) Internals
+// ───────────────────────────────────────────────────────────────────────────────
+
+/**
+ * Resolve place -> lat/lon via Open-Meteo Geocoding and fetch a 7-day daily forecast.
+ * @param string $place
+ * @return array context with:
+ * name (City, Country), tz, daily => [keys matching Open-Meteo daily params]
+ * or ['error' => 'message']
+ */
+function wm_build_context(string $place): array {
+ $place = trim($place);
+ if ($place === '') return ['error' => "Please provide a location (e.g., 'Lisbon')."];
+
+ $geo = wm_http_json("https://geocoding-api.open-meteo.com/v1/search?name=" . urlencode($place) . "&count=1&language=en&format=json");
+ if (!$geo || empty($geo['results'][0])) {
+ return ['error' => "Could not find location for '$place'."];
+ }
+
+ $g = $geo['results'][0];
+ $lat = $g['latitude'];
+ $lon = $g['longitude'];
+ $displayName = $g['name'] . (isset($g['country']) ? ", " . $g['country'] : "");
+
+ // Daily parameters
+ $dailyParams = [
+ 'weathercode',
+ 'temperature_2m_max',
+ 'temperature_2m_min',
+ 'precipitation_sum',
+ 'precipitation_probability_max',
+ 'windspeed_10m_max'
+ ];
+
+ $forecastUrl = "https://api.open-meteo.com/v1/forecast?"
+ . http_build_query([
+ 'latitude' => $lat,
+ 'longitude' => $lon,
+ 'daily' => implode(',', $dailyParams),
+ 'forecast_days' => 7,
+ 'timezone' => 'auto'
+ ]);
+
+ $fc = wm_http_json($forecastUrl);
+ if (!$fc || empty($fc['daily'])) {
+ return ['error' => "Could not fetch forecast for '$displayName'."];
+ }
+
+ $tz = $fc['timezone'] ?? 'local';
+ return [
+ 'name' => $displayName,
+ 'tz' => $tz,
+ 'daily' => $fc['daily'],
+ ];
+}
+
+/**
+ * Simple GET JSON helper with cURL
+ * @param string $url
+ * @return array|null
+ */
+function wm_http_json(string $url): ?array {
+ $ch = curl_init($url);
+ curl_setopt_array($ch, [
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_TIMEOUT => 10,
+ CURLOPT_USERAGENT => "WeatherModule/1.0 (+github.com/you)"
+ ]);
+ $res = curl_exec($ch);
+ $err = curl_error($ch);
+ curl_close($ch);
+
+ if ($res === false || !$res) return null;
+ $data = json_decode($res, true);
+ return is_array($data) ? $data : null;
+}
+
+/**
+ * Format a single-day verbose summary.
+ */
+function wm_format_day_summary(string $title, string $dateLabel, array $d, int $i): string {
+ $code = $d['weathercode'][$i] ?? null;
+ $desc = wm_weathercode_text($code);
+ $icon = wm_weathercode_emoji($code);
+
+ $tmax = wm_num($d['temperature_2m_max'][$i] ?? null);
+ $tmin = wm_num($d['temperature_2m_min'][$i] ?? null);
+ $pp = wm_num($d['precipitation_probability_max'][$i] ?? null);
+ $pr = wm_num($d['precipitation_sum'][$i] ?? null);
+ $wmax = wm_num($d['windspeed_10m_max'][$i] ?? null);
+
+ $parts = [];
+ $parts[] = "$title — $dateLabel";
+ $parts[] = "$icon $desc";
+ if ($tmax !== null && $tmin !== null) $parts[] = "Temp: min {$tmin}°C / max {$tmax}°C";
+ if ($pp !== null) $parts[] = "Precip prob: {$pp}%";
+ if ($pr !== null) $parts[] = "Precip total: {$pr} mm";
+ if ($wmax !== null) $parts[] = "Wind up to: {$wmax} m/s";
+
+ return implode("\n", $parts);
+}
+
+/**
+ * Format a compact weekly line for a given day.
+ */
+function wm_format_compact_line(string $label, array $d, int $i): string {
+ $code = $d['weathercode'][$i] ?? null;
+ $icon = wm_weathercode_emoji($code);
+
+ $tmax = wm_num($d['temperature_2m_max'][$i] ?? null);
+ $tmin = wm_num($d['temperature_2m_min'][$i] ?? null);
+ $pp = wm_num($d['precipitation_probability_max'][$i] ?? null);
+
+ $temp = ($tmin !== null && $tmax !== null) ? "{$tmin}–{$tmax}°C" : "n/a";
+ $prob = ($pp !== null) ? "{$pp}%" : "n/a";
+
+ return sprintf("%s %s %s (rain prob %s)", $label, $icon, $temp, $prob);
+}
+
+/**
+ * Weather code -> short text
+ * Ref: https://open-meteo.com/en/docs
+ */
+function wm_weathercode_text(?int $code): string {
+ $map = [
+ 0 => "Clear sky",
+ 1 => "Mainly clear",
+ 2 => "Partly cloudy",
+ 3 => "Overcast",
+ 45 => "Fog",
+ 48 => "Depositing rime fog",
+ 51 => "Light drizzle",
+ 53 => "Moderate drizzle",
+ 55 => "Dense drizzle",
+ 56 => "Freezing drizzle (light)",
+ 57 => "Freezing drizzle (dense)",
+ 61 => "Slight rain",
+ 63 => "Moderate rain",
+ 65 => "Heavy rain",
+ 66 => "Freezing rain (light)",
+ 67 => "Freezing rain (heavy)",
+ 71 => "Slight snow fall",
+ 73 => "Moderate snow fall",
+ 75 => "Heavy snow fall",
+ 77 => "Snow grains",
+ 80 => "Rain showers (slight)",
+ 81 => "Rain showers (moderate)",
+ 82 => "Rain showers (violent)",
+ 85 => "Snow showers (slight)",
+ 86 => "Snow showers (heavy)",
+ 95 => "Thunderstorm (slight/moderate)",
+ 96 => "Thunderstorm with slight hail",
+ 99 => "Thunderstorm with heavy hail",
+ ];
+ return $map[$code] ?? "Unknown conditions";
+}
+
+/**
+ * Weather code -> emoji
+ */
+function wm_weathercode_emoji(?int $code): string {
+ if ($code === null) return "🌡️";
+ if ($code === 0) return "☀️";
+ if (in_array($code, [1,2])) return "⛅";
+ if ($code === 3) return "☁️";
+ if (in_array($code, [45,48])) return "🌫️";
+ if (in_array($code, [51,53,55,61,63,65,80,81,82])) return "🌧️";
+ if (in_array($code, [66,67])) return "🌨️🧊";
+ if (in_array($code, [71,73,75,77,85,86])) return "❄️";
+ if (in_array($code, [95,96,99])) return "⛈️";
+ return "🌡️";
+}
+
+/**
+ * Pretty date label (e.g., 2025-09-09 → Tue 9 Sep)
+ */
+function wm_pretty_date(string $isoDate, string $tz): string {
+ try {
+ $dt = new DateTime($isoDate, new DateTimeZone($tz ?: 'UTC'));
+ return $dt->format('D j M Y');
+ } catch (Throwable $e) {
+ return $isoDate;
+ }
+}
+
+/**
+ * Weekday + date (e.g., Tue 2025-09-09)
+ */
+function wm_pretty_weekday(string $isoDate, string $tz): string {
+ try {
+ $dt = new DateTime($isoDate, new DateTimeZone($tz ?: 'UTC'));
+ return $dt->format('D Y-m-d');
+ } catch (Throwable $e) {
+ return $isoDate;
+ }
+}
+
+/**
+ * Round number nicely or return null
+ */
+function wm_num($v): ?string {
+ if ($v === null) return null;
+ if ($v === '' || !is_numeric($v)) return null;
+ return (string)round((float)$v, 1);
+}
diff --git a/lib/actions/wikipedia.action.php b/lib/actions/wikipedia.action.php
new file mode 100644
index 0000000..0359a6e
--- /dev/null
+++ b/lib/actions/wikipedia.action.php
@@ -0,0 +1,131 @@
+ → returns the lead/summary paragraph
+ * @wikiFullArticle → returns the full article as plain text
+ *
+ * Examples:
+ * @wikiSummary Finland
+ * @wikiFullArticle Bitcoin
+ */
+
+// 1) Register actions
+registerAction(
+ (new Action())
+ ->setFunctionName("wikiSummary")
+ ->setArgumentsTypes(["string"]) // title
+);
+
+registerAction(
+ (new Action())
+ ->setFunctionName("wikiFullArticle")
+ ->setArgumentsTypes(["string"]) // title
+);
+
+// --- Helpers ---------------------------------------------------------------
+
+/**
+ * Basic HTTP GET via cURL.
+ * @return array [int $httpCode, ?string $body, ?string $err]
+ */
+function http_get(string $url, int $timeout = 6): array {
+ $ch = curl_init($url);
+ curl_setopt_array($ch, [
+ CURLOPT_RETURNTRANSFER => true,
+ CURLOPT_FOLLOWLOCATION => true,
+ CURLOPT_TIMEOUT => $timeout,
+ CURLOPT_CONNECTTIMEOUT => 4,
+ CURLOPT_USERAGENT => "LLM-Action-Demo/1.0 (+https://example.com)"
+ ]);
+ $body = curl_exec($ch);
+ $err = curl_error($ch) ?: null;
+ $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
+ curl_close($ch);
+ return [$code, $body, $err];
+}
+
+/** Safely pick the first (and only) page object from MediaWiki Action API */
+function mw_first_page(array $json): ?array {
+ if (!isset($json['query']['pages']) || !is_array($json['query']['pages'])) return null;
+ foreach ($json['query']['pages'] as $page) {
+ return $page; // first element
+ }
+ return null;
+}
+
+// --- Actions ---------------------------------------------------------------
+
+/**
+ * @param string $title Wikipedia article title
+ * @return string
+ */
+function wikiSummary(string $title): string {
+ $title = str_replace("-"," ",$title);
+ $encoded = rawurlencode($title);
+ $url = "https://en.wikipedia.org/api/rest_v1/page/summary/{$encoded}";
+
+ [$code, $body, $err] = http_get($url);
+ if ($err) return "Error fetching summary for '{$title}': {$err}";
+ if ($code < 200 || $code >= 300 || !$body) return "HTTP {$code}: Failed to fetch summary for '{$title}'.";
+
+ $data = json_decode($body, true);
+ if (isset($data['extract']) && is_string($data['extract']) && $data['extract'] !== '') {
+ return $data['extract'];
+ }
+
+ // Common “not found” or disambiguation handling
+ if (!empty($data['type']) && $data['type'] === 'disambiguation') {
+ return "‘{$title}’ is a disambiguation page. Try a more specific title.";
+ }
+
+ return "No summary found for '{$title}'.";
+}
+
+/**
+ * Returns full article as plain text (sections + paragraphs).
+ * Uses MediaWiki Action API with extracts (plaintext).
+ *
+ * @param string $title
+ * @return string
+ */
+function wikiFullArticle(string $title): string {
+ $title = str_replace("-"," ",$title);
+ $encoded = rawurlencode($title);
+ $url = "https://en.wikipedia.org/w/api.php"
+ . "?action=query"
+ . "&prop=extracts"
+ . "&explaintext=1"
+ . "&exsectionformat=plain"
+ . "&format=json"
+ . "&redirects=1"
+ . "&titles={$encoded}";
+
+ [$code, $body, $err] = http_get($url, 12);
+ if ($err) return "Error fetching article for '{$title}': {$err}";
+ if ($code < 200 || $code >= 300 || !$body) return "HTTP {$code}: Failed to fetch article for '{$title}'.";
+
+ $json = json_decode($body, true);
+ $page = mw_first_page($json);
+
+ if (!$page) {
+ return "No article found for '{$title}'.";
+ }
+
+ if (isset($page['missing'])) {
+ return "No article found for '{$title}'.";
+ }
+
+ if (!isset($page['extract']) || trim($page['extract']) === '') {
+ return "Article exists but has no plain-text extract for '{$title}'.";
+ }
+
+ // Optionally trim extremely long responses (LLM-friendly)
+ $maxChars = 40000; // adjust for your pipeline
+ $text = $page['extract'];
+ if (mb_strlen($text, 'UTF-8') > $maxChars) {
+ $text = mb_substr($text, 0, $maxChars, 'UTF-8') . "\n\n[Truncated]";
+ }
+
+ return $text;
+}
diff --git a/tests/01_test.php b/tests/01_test.php
new file mode 100644
index 0000000..ed99f8c
--- /dev/null
+++ b/tests/01_test.php
@@ -0,0 +1,7 @@
+[["role"=>"user","content"=>"What is the capital of France?"],["role"=>"assistant","content"=>"Paris."]]]);
+print_r($out);
+
+
+
+
diff --git a/tests/03_system_major_role.php b/tests/03_system_major_role.php
new file mode 100644
index 0000000..ffe699e
--- /dev/null
+++ b/tests/03_system_major_role.php
@@ -0,0 +1,8 @@
+"user","content"=>$msg];
+$conv_hist[] = ["role"=>"assistant","content"=>($chapters[] = LlamaCli_raw($msg,$sys_msg,["debug_level"=>0]))];
+
+for($no=1; $no < $total_no_of_chapters; $no++) {
+ $msg = "Write me the title of chapter number $no.";
+ $conv_hist[] = ["role"=>"user","content"=>$msg];
+ $conv_hist[] = ["role"=>"assistant","content"=>($chapters[] = LlamaCli_raw($msg,$sys_msg,["previousConversation"=>$conv_hist, "debug_level"=>0]))];
+
+}
+
+// 3. CHAPTER CONTENTS
+$content = [];
+foreach($chapters as $chapter_title)
+ $content[$chapter_title] = LlamaCli_raw(
+ "Write 2 paragraphs for a chapter titled $chapter_title in a book called $book_title. Output content only, no chat.",
+ "You are an expert $expert."
+ );
+
+print_r([$book_title, $content]);
diff --git a/tests/05_ChainedQuery_ProduceShortContentFromLongContent.php b/tests/05_ChainedQuery_ProduceShortContentFromLongContent.php
new file mode 100644
index 0000000..5011937
--- /dev/null
+++ b/tests/05_ChainedQuery_ProduceShortContentFromLongContent.php
@@ -0,0 +1,12 @@
+