new version

This commit is contained in:
Frederico @ VilaRosa02 2025-09-10 11:40:03 +00:00
parent 2e01e34278
commit 436e0e57c5
12 changed files with 641 additions and 36 deletions

View File

@ -1,45 +1,50 @@
<?php <?php
function ResolveActions(string $text) {
$actions = [];
class Action {
private $function_name;
private $no_of_args;
private $args_type;
if (preg_match("/@rss_reader ([\w:\/\.]+)/",$text,$matches)) { public function setFunctionName(string $s) { $this->function_name = $s; return $this; }
$url = $matches[1]; 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; }
// Initialize cURL public function getFunctionName() { return $this->function_name; }
$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 <title>
$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;
} }
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:\/\.-]+)";
$text = str_replace($matches[0],$firstTitle,$text); if (preg_match("/$function_name_regex_format( |$)/",$text,$matches)) {
$function_name = $matches[1];
if ( !isset($actions[$function_name]) ||
!function_exists($function_name )) return "[[ Action $function_name not available. ]]";
$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; return $text;

View File

@ -0,0 +1,30 @@
<?php
/**
* SAMPLE ACTION: Say Hello
* -------------------------
* Demonstrates how to register and implement a simple action.
*
* Call format:
* @sayHello Alice 30
*
* Expected output:
* "Hello Alice, you are 30 years old!"
*/
// 1. Register the action with expected argument types
registerAction(
(new Action())
->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!";
}

6
lib/actions/index.php Normal file
View File

@ -0,0 +1,6 @@
<?php
foreach(scandir(__DIR__) as $filename) {
if ($filename == "." || $filename == "..") continue;
if (str_ends_with($filename, ".action.php")) include __DIR__."/$filename";
}

View File

@ -0,0 +1,40 @@
<?php
registerAction((new Action())->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 <title>
$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;
}

View File

@ -0,0 +1,303 @@
<?php
/**
* MODULE: Weather (Open-Meteo, no API key)
* ---------------------------------------
* Provides three actions:
* @weather_today Lisbon
* @weather_tomorrow Lisbon
* @weather_this_week Lisbon
*
* Uses Open-Meteo Geocoding + Forecast APIs (free, public).
* Returns human-readable summaries (temps °C, precip, wind + weather emoji).
*/
// ───────────────────────────────────────────────────────────────────────────────
// 1) Register actions
// ───────────────────────────────────────────────────────────────────────────────
registerAction(
(new Action())
->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);
}

View File

@ -0,0 +1,131 @@
<?php
/**
* ACTIONS: Wikipedia Integration
* ------------------------------
* @wikiSummary <Title> returns the lead/summary paragraph
* @wikiFullArticle <Title> 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;
}

7
tests/01_test.php Normal file
View File

@ -0,0 +1,7 @@
<?php require_once __DIR__."/../LlamaCli.func.php";
// TEST 01: Simple query.
$out = LlamaCli("What is the capital of France?"); print_r($out);

View File

@ -0,0 +1,11 @@
<?php require_once __DIR__."/../LlamaCli.func.php";
// TEST 02: Simple query with previous background conversation
$out = LlamaCli("Portugal?","",["previousConversation"=>[["role"=>"user","content"=>"What is the capital of France?"],["role"=>"assistant","content"=>"Paris."]]]);
print_r($out);

View File

@ -0,0 +1,8 @@
<?php require_once __DIR__."/../LlamaCli.func.php";
// TEST 03: Simple query with SYSTEM PRIMING message
$summary = LlamaCli(file_get_contents("test_input_data.3.txt"),"You are an executive assistant that will summarize english text in 1 paragraph");
$title = LlamaCli($summary[0],"You are an talented journalist that will produce a provocative headline title based on a summary of a text");
print_r([$summary,$title]);

View File

@ -0,0 +1,40 @@
<?php require_once __DIR__."/../LlamaCli.func.php";
TEST 04: RECURSIVE QUERY, to produce long-content from short-one. LONG-WRITING. ( GOAL: Learning from model deep knowledge )
// -------- BOOK EXAMPLE ---------
// 0. CATEGORY
$category = "learning finnish";
$expert = "teacher";
$total_no_of_chapters = 3;
// 1. TITLE
$book_title = LlamaCli_raw("Write me a title for my new book about $category. Output title only, no options, no chat.","You are an expert $expert.");
// 2. CHAPTER TITLES
$sys_msg = "You are an expert $expert writing a book with the title $book_title with $total_no_of_chapters chapters";
$chapters = []; $conv_hist = [];
$msg = "Write me the title of my first chapter. Output title only, no options, no chat.";
$conv_hist[] = ["role"=>"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]);

View File

@ -0,0 +1,12 @@
<?php require_once __DIR__."/../LlamaCli.func.php";
// TEST 05: CHAINED QUERY, to parse long-content and produce short one. SUMMARIZATION. ( GOAL: )
$summaries = [];
$content = file_get_contents("test_input_data.2.txt");
$chunks = array_chunk(explode("\n",$content),80);
foreach($chunks as $chunk) {
$summaries[] = LlamaCli_raw(implode("\n",$chunk),"You are a legal executive assistant that will summarize a conversation in english between ex-husband and wife in 1 paragraph");
}
print_r($summaries);

View File

@ -0,0 +1,12 @@
<?php require_once __DIR__."/../LlamaCli.func.php";
// TEST 06: Resolve FETCHING ACTIONS
//echo LlamaCli_raw("translate this title to portuguese and finnish and quote the original in english: @getFirstTitleFromRSSFeed https://www.hs.fi/rss/tuoreimmat.xml","");
//echo LlamaCli_raw("translate this wiki summary to portuguse and show the result also in english @wikiFullArticle Bitcoin","");
// echo LlamaCli_raw("Give me a one paragraph description of what you know about Pablo Escobar","");
// echo LlamaCli_raw("did you know this about Pablo Escobar? @wikiSummary Pablo-Escobar \n\n\nlet me know your impressions.","");
echo LlamaCli_raw("Based on this forecasted weather for tomorrow @weather_tomorrow Lisbon give me a plan on what to do, since you know I'm on vacation","");