Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Dynamic help from agents #333

Merged
merged 4 commits into from
Dec 5, 2024
Merged
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
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.14.0
1.15.0
6 changes: 4 additions & 2 deletions src/main/groovy/org/arl/fjage/shell/BaseGroovyScript.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -495,7 +495,8 @@ abstract class BaseGroovyScript extends Script {
Binding binding = getBinding()
if (binding.hasVariable('__doc__')) {
def doc = binding.getVariable('__doc__')
return doc.get(obj as String)
Agent a = binding.hasVariable('__agent__') ? binding.getVariable('__agent__') : null
return doc.get(a, obj as String)
}
return null
}
Expand All @@ -509,7 +510,8 @@ abstract class BaseGroovyScript extends Script {
Binding binding = getBinding()
if (binding.hasVariable('__doc__')) {
def doc = binding.getVariable('__doc__')
return doc.get()
Agent a = binding.hasVariable('__agent__') ? binding.getVariable('__agent__') : null
return doc.get(a)
}
return null
}
Expand Down
147 changes: 116 additions & 31 deletions src/main/java/org/arl/fjage/shell/Documentation.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,81 +13,116 @@
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.arl.fjage.*;
import org.arl.fjage.param.*;

public class Documentation {

protected List<String> doc = new ArrayList<String>();
protected Map<String,Integer> ndx = new HashMap<String,Integer>();
protected Pattern heading = Pattern.compile("^#+ +([^ ]+) +-.*$");
protected Pattern section = Pattern.compile("^#+ .*$");
protected Map<String,String> cache = new HashMap<String,String>();
protected int staticSize = 0;

protected static final Pattern heading = Pattern.compile("^#+ +([^ ]+) +-.*$");
protected static final Pattern section = Pattern.compile("^#+ (.*)$");
protected static final String crlf = "\\r?\\n";
protected static final String header = "^#+ +";
protected static final String gaps = "\\n+$";
protected static final String placeholder = "@@"; // to be replaced by agent name

/**
* Add markdown documentation.
*
* @param s multiline string.
*/
public void add(String s) {
String[] lines = s.split("\\r?\\n");
for (String line: lines) {
Matcher m = heading.matcher(line);
if (m.matches()) ndx.put(m.group(1), doc.size());
if (doc.size() > staticSize) doc.subList(staticSize, doc.size()).clear();
String[] lines = s.split(crlf);
for (String line: lines)
doc.add(line);
}
staticSize = doc.size();
}

/**
* Get documentation index.
*/
public String get() {
public String get(Agent agent) {
build(agent);
List<String> topics = new ArrayList<String>();
for (String s: doc)
if (s.startsWith("# ")) topics.add(s.substring(2));
topics.sort(null);
StringBuilder sb = new StringBuilder();
for (Integer v: ndx.values()) {
String s = doc.get(v);
if (s.startsWith("# ")) {
sb.append(s.substring(2));
sb.append('\n');
}
for (String s: topics) {
sb.append(s);
sb.append('\n');
}
return sb.toString();
}

/**
* Get documentation by keyword.
*
* @param keyword keyword.
*/
public String get(Agent agent, String keyword) {
build(agent);
int count = 0;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < doc.size(); i++) {
String s = doc.get(i);
Matcher m = heading.matcher(s);
if ((m.matches() && m.group(1).equals(keyword))) {
extract(sb, i);
count++;
continue;
}
m = section.matcher(s);
if (m.matches() && m.group(1).equals(keyword)) {
extract(sb, i);
count++;
continue;
}
}
if (count == 0) search(sb, keyword);
return sb.toString().replaceAll(gaps, "\n");
}

/**
* Search documentation topics by keyword.
*
* @param sb string builder.
* @param keyword keyword.
*/
public String search(String keyword) {
protected void search(StringBuilder sb, String keyword) {
keyword = keyword.toLowerCase();
Set<String> topics = new HashSet<String>();
String topic = null;
for (String s: doc) {
Matcher m = section.matcher(s);
if (m.matches()) topic = s.replaceAll("^#+ +", "- ");
if (m.matches()) topic = s.replaceAll(header, "- ");
if (s.toLowerCase().contains(keyword)) topics.add(topic);
}
if (topics.size() == 0) return null;
StringBuilder sb = new StringBuilder();
if (topics.size() == 0) return;
sb.append("Possible topics:\n");
for (String s: topics) {
sb.append(s);
sb.append('\n');
}
return sb.toString();
}

/**
* Get documentation by keyword.
* Extract documentation.
*
* @param keyword keyword.
* @param sb string builder.
* @param pos position.
*/
public String get(String keyword) {
Integer pos = ndx.get(keyword);
if (pos == null) return search(keyword);
protected void extract(StringBuilder sb, int pos) {
String s = doc.get(pos);
int level = s.indexOf(' ');
if (level <= 0) return null;
if (level <= 0) return;
int endlevel = level;
StringBuilder sb = new StringBuilder();
sb.append(s.replaceAll("^#+ +", ""));
if (s.startsWith("# ")) sb.append(s);
else sb.append(s.replaceAll(header, ""));
sb.append('\n');
int skip = 0;
for (int i = pos+1; i < doc.size(); i++) {
Expand All @@ -101,11 +136,11 @@ public String get(String keyword) {
if (skip == 0 || level <= skip) {
if (skip > 0) nl = true;
skip = 0;
s = s.replaceAll("^#+ +", "");
s = s.replaceAll(header, "");
}
if (m.matches()) {
skip = level;
sb.append("- ");
sb.append("* ");
sb.append(s);
sb.append('\n');
} else if (nl) {
Expand All @@ -117,7 +152,57 @@ public String get(String keyword) {
sb.append('\n');
}
}
return sb.toString().replaceAll("\\n+$", "\n");
sb.append('\n');
}

/**
* Build dynamic documentation by querying agents. Documentation may be
* cached for efficiency. If an agent wishes to avoid caching, it should
* return a documentation string that starts with "[no-cache]".
* <p>
* Agents with names with special characters (e.g. hyphen) are not
* queried for documentation. This is to cater for gateway agents that do not
* support any requests. Such agents should always use special characters in
* their advertised names.
*
* @param agent agent.
*/
protected void build(Agent agent) {
if (agent == null) return;
if (doc.size() > staticSize) doc.subList(staticSize, doc.size()).clear();
Container c = agent.getContainer();
AgentID[] agentIDs = c.agentsForService("org.arl.fjage.shell.Services.DOCUMENTATION");
for (AgentID a: agentIDs) {
String key = a.getName();
if (a.getType() != null) key += "::" + a.getType();
if (cache.containsKey(key)) {
String s = cache.get(key);
if (s != null) {
String[] lines = s.split(crlf);
for (String line: lines)
doc.add(line);
}
continue;
}
ParameterReq req = new ParameterReq();
req.setRecipient(a);
req.get(new NamedParameter("__doc__"));
Message rsp = agent.request(req, 1000);
if (rsp != null && rsp instanceof ParameterRsp) {
Object docstr = ((ParameterRsp)rsp).get(new NamedParameter("__doc__"));
if (docstr != null) {
String s = docstr.toString().replaceAll(placeholder, a.getName());
if (s.startsWith("[no-cache]")) s = s.substring(10);
else cache.put(key, s);
String[] lines = s.split(crlf);
for (String line: lines)
doc.add(line);
}
} else {
// no documentation available for agent
cache.put(key, null);
}
}
}

}
3 changes: 2 additions & 1 deletion src/main/java/org/arl/fjage/shell/Services.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@
* Services supported by fjage shell agents.
*/
public enum Services {
SHELL
SHELL,
DOCUMENTATION
}
Loading