Query hangs in swiplserver, timeout doesn't work

I’m using the latest versions of everything, and I have a query that hangs when I run it through swiplserver, but runs fine when I run the equivalent code in SWISH.

Here’s the python version

from swiplserver import PrologMQI
import tempfile

rules = """
:- use_module(library(scasp)).
:- use_module(library(scasp/human)).
:- use_module(library(scasp/output)).

:- meta_predicate
    blawxrun(0,-).

blawxrun(Query, Human) :-
    scasp(Query,[tree(Tree)]),
ovar_analyze_term(t(A, Tree)),ovar_analyze_term(t(B, Tree)),
    with_output_to(string(Human),
    human_justification_tree(Tree,[])).

#pred overrules(R1,R2) :: 'the conclusion in @(R1) overrules the conclusion in @(R2)'.
#pred opposes(C1,C2) :: 'the conclusion @(C1) opposes the conclusion @(C2)'.
#pred defeated(R,_) :: 'the conclusion in @(R) is defeated'.
#pred refuted(R,_) :: 'the conclusion in @(R) is refuted'.

refuted(R,C) :-
    opposes(C,OC),
    overrules(OR,R),
    according_to(OR,OC).

defeated(R,C) :-
    refuted(R,C).

legally_holds(R,C) :-
    according_to(R,C),
    not defeated(R,C). 






#pred game(X) :: '@(X) is a game'.
#pred player(X) :: '@(X) is a player'.
#pred player(Y,X) :: '@(X) played in @(Y)'.

#pred sign(X) :: '@(X) is a sign'.

sign(rock).

sign(paper).

sign(scissors).

beats(rock,scissors).

#pred beats(X,Y) :: '@(X) beats @(Y)'.

beats(paper,rock).

beats(scissors,paper).

#pred winner(X,Y) :: 'the winner of @(X) is @(Y)'.

#pred throw(X,Y) :: '@(X) threw @(Y)'.


winner(Game,Player1) :-
player(Game,Player1),
player(Game,Player2),
throw(Player1,Throw1),
throw(Player2,Throw2),
beats(Throw1,Throw2).

#abducible player(B,A).

#abducible throw(A,B).

?- winner(A,B).
"""
rulefile = tempfile.NamedTemporaryFile('w',delete=False)
rulefile.write(rules)
rulefile.close()
rulefilename = rulefile.name
query = "blawxrun(winner(A,B),Human)."


with PrologMQI() as swipl:
    with swipl.create_thread() as swipl_thread:
        load_file_answer = swipl_thread.query("['" + rulefilename + "'].",query_timeout_seconds=1)
        query_answer = swipl_thread.query(query,query_timeout_seconds=1)

print(load_file_answer)
print(query_answer)

Swiplserver seems to go into an infinite loop at _receive on the line starting with query_answer =. I have tried various timeouts to see if I could get it to throw an error, to no avail.

If you remove the :- meta_predicate entry and blawxrun, and query winner(A,B). in SWISH, it works fine. Here is a link to a demonstration in SWISH of what I’m expecting to receive.

I’m a little stuck as to what I can do to troubleshoot it, from there. I feel like the last time I ran into this problem (infinite loops in swiplserver _receive), @jan gave me a solution, and then suggested that it was not a permanent interface. Perhaps something has changed relevant to how I’m implementing the blawxrun\2 definition? But most code still works fine. It’s just code with #abducible statements that loops infinitely. So I suspect it is something about how ovar_analyse_term or human_justification_tree is working when the answer includes something that is particular to abduicbility?

Happy to dig into it further, but I honestly don’t even know how. :slight_smile:

Looks like an MQI issue. Runs also fine in plain Prolog without SWISH.

1 Like

The problem seems to only happen when
a) there is an #abudicble statement using a variable, and
b) I include the ovar_analyze_term call in the blawxrun definition.

For now I’m going to remove the ovar_analyze_term and make an issue to come back to reformatting abducibility explanations.

What should I expect to not work in the meantime?

I tried it without the ovar statement, and the queries with abducibility now return an answer, though the formatting is a bit wild. But other queries fail with the determinism bug I reported earlier.

Not sure where to go from here.

Can you generate something comprehensive and easy to reproduce the problem?

I’m struggling to find a simple example that has the problem I’m describing. I’ll get back to you when I can find one.

This is the file:

:- use_module(library(scasp)).
:- use_module(library(scasp/human)).
:- use_module(library(scasp/output)).

:- meta_predicate
    blawxrun(0,-).

blawxrun(Query, Human) :-
    scasp(Query,[tree(Tree)]),
    ovar_analyze_term(t(A, Tree),[name_constraints(true)]), ovar_analyze_term(t(B, Tree),[name_constraints(true)]),
    with_output_to(string(Human),
    human_justification_tree(Tree,[])).

#pred overrules(R1,R2) :: 'the conclusion in @(R1) overrules the conclusion in @(R2)'.
#pred opposes(C1,C2) :: 'the conclusion @(C1) opposes the conclusion @(C2)'.
#pred defeated(R,_) :: 'the conclusion in @(R) is defeated'.
#pred refuted(R,_) :: 'the conclusion in @(R) is refuted'.

refuted(R,C) :-
    opposes(C,OC),
    overrules(OR,R),
    according_to(OR,OC).

defeated(R,C) :-
    refuted(R,C).

legally_holds(R,C) :-
    according_to(R,C),
    not defeated(R,C). 






#pred game(X) :: '@(X) is a game'.
#pred player(X) :: '@(X) is a player'.
#pred player(Y,X) :: '@(X) played in @(Y)'.

#pred sign(X) :: '@(X) is a sign'.

sign(rock).

sign(paper).

sign(scissors).

beats(rock,scissors).

#pred beats(X,Y) :: '@(X) beats @(Y)'.

beats(paper,rock).

beats(scissors,paper).

#pred winner(X,Y) :: 'the winner of @(X) is @(Y)'.

#pred throw(X,Y) :: '@(X) threw @(Y)'.


winner(Game,Player1) :-
player(Game,Player1),
player(Game,Player2),
throw(Player1,Throw1),
throw(Player2,Throw2),
beats(Throw1,Throw2).

#abducible player(B,A).

#abducible throw(A,B).

The query is blawxrun(winner(A,B),Human). It succeeds at top level, but fails with a determinism error on html_justification_tree if you turn on debug. I’m not sure what to make of that, but that doesn’t seem to be the problem I’m currently fighting.

I then ran MQI in standalone as suggested in the docs using mqi_start([port(5000),password('test")]). and running the debug commands.
Then I ran the code above with the following python:

rules = """
... (as above)
"""
rulefile = tempfile.NamedTemporaryFile('w',delete=False)
rulefile.write(rules)
rulefile.close()
rulefilename = rulefile.name

try:
    with PrologMQI(launch_mqi=False, port=5000, password="test") as swipl:
        with swipl.create_thread() as swipl_thread:
            load_file_answer = swipl_thread.query("['" + rulefilename + "'].")
            query_answer = swipl_thread.query("blawxrun(winner(A,B),Human).")
            print(query_answer)
except PrologError as err:
    print(err)

In the output I see the following error:

% [Thread mqi2_conn2_comm] Session finished. Communication thread exception: error(type_error(free_of_attvar,true( ... ),context(system:numbervars/4,_6164))
% [Thread mqi2_conn2_comm] Attempting to abort thread: mqi2_conn2_goal. thread_signal_exception: _6306
% [Thread mqi2_conn2_goal] Thread mqi2_conn2_goal exited with status exception($aborted)
% [Thread mqi2_conn2_comm] Ending session mqi2_conn2_comm
% [Thread mqi2_conn2_comm] Thread mqi2_conn2_comm exited with status true
% [Thread mqi2_conn2_comm] Expected thread status, detaching thread mqi2_conn2_comm

where ... is the (long) content of the query response.

free_of_attvar reminds me that if you run the code at top level, you get the following put_attr lines at the end of the output…

?- blawxrun(winner(A,B),Human).
Human = "   the winner of A is B, because\n      B played in A, because\n         there is no evidence that o_player holds for A, and B, because\n            it is assumed that B played in A\n         abducible$ holds for player(_18200,_18218), because\n            there is no evidence that abducible$$ holds for player(_18200,_18218), because\n               abducible$ holds for player(_18200,_18218), because\n                  it is assumed that there is no evidence that abducible$$ holds for player(_18200,_18218)\n      C played in A, because\n         there is no evidence that o_player holds for A, and C, because\n            it is assumed that C played in A\n         abducible$ holds for player(_18200,_18236), because\n            there is no evidence that abducible$$ holds for player(_18200,_18236), because\n               abducible$ holds for player(_18200,_18236), because\n                  it is assumed that there is no evidence that abducible$$ holds for player(_18200,_18236)\n      B threw rock, because\n         there is no evidence that o_throw holds for B, and rock, because\n            it is assumed that B threw rock\n         abducible$ holds for throw(_18218,rock), because\n            there is no evidence that abducible$$ holds for throw(_18218,rock), because\n               abducible$ holds for throw(_18218,rock), because\n                  it is assumed that there is no evidence that abducible$$ holds for throw(_18218,rock)\n      C threw scissors, because\n         there is no evidence that o_throw holds for C, and scissors, because\n            it is assumed that C threw scissors\n         abducible$ holds for throw(_18236,scissors), because\n            there is no evidence that abducible$$ holds for throw(_18236,scissors), because\n               abducible$ holds for throw(_18236,scissors), because\n                  it is assumed that there is no evidence that abducible$$ holds for throw(_18236,scissors)\n      rock beats scissors\n   ∎\n",
% s(CASP) model
{ beats(rock,scissors),      not o_throw(_A,scissors),  throw(_A,scissors),
  not o_player(A,B),         player(A,B),               winner(A,B),
  not o_player(A,_A),        player(A,_A),
  not o_throw(B,rock),       throw(B,rock)
},
% s(CASP) justification
query ←
   winner(A,B) ←
      player(A,B) ←
         not o_player(A,B) ←
            chs(player(A,B)) ∧
         'abducible$'(player(A,B)) ←
            not 'abducible$$'(player(A,B)) ←
               'abducible$'(player(A,B)) ←
                  chs(not 'abducible$$'(player(A,B))) ∧
      player(A,_A) ←
         not o_player(A,_A) ←
            chs(player(A,_A)) ∧
         'abducible$'(player(A,_A)) ←
            not 'abducible$$'(player(A,_A)) ←
               'abducible$'(player(A,_A)) ←
                  chs(not 'abducible$$'(player(A,_A))) ∧
      throw(B,rock) ←
         not o_throw(B,rock) ←
            chs(throw(B,rock)) ∧
         'abducible$'(throw(B,rock)) ←
            not 'abducible$$'(throw(B,rock)) ←
               'abducible$'(throw(B,rock)) ←
                  chs(not 'abducible$$'(throw(B,rock))) ∧
      throw(_A,scissors) ←
         not o_throw(_A,scissors) ←
            chs(throw(_A,scissors)) ∧
         'abducible$'(throw(_A,scissors)) ←
            not 'abducible$$'(throw(_A,scissors)) ←
               'abducible$'(throw(_A,scissors)) ←
                  chs(not 'abducible$$'(throw(_A,scissors))) ∧
      beats(rock,scissors),
put_attr(A, scasp_output, name('A')),
put_attr(B, scasp_output, name('B')),
put_attr(_A, scasp_output, name('C'))

The put_attr lines do not occur if you omit the ovar_analyze_term line from the definition of blawxrun.

So working theory:

  1. MQI is gracelessly terminating the connection when the type error is detected without advising the client, and
  2. Either or both of:
    (a) MQI is failing to deal with information in the results that belongs, or,
    (b) ovar_analyze_term is adding information to the results that shouldn’t be there.
  3. There may be a determinism problem with html_justification_tree.

If I can figure out how to troubleshoot further, I will. Thanks for the help.

Ok. With some work this reproduced (please make sure the Python file is complete). Here is the working file (named .py.pl as .py is not accepted).

run.py.pl (2.1 KB)

The main change is that I kill the attributes in blawxrun/2. I also do not see the reason to call ovar_analyze_term/2 twice and you need to pass a term holding all relevant data, so the Query and the Tree:

blawxrun(Query, Human) :-
    scasp(Query,[tree(Tree)]),
    ovar_analyze_term(t(Query, Tree),[name_constraints(true)]),
    with_output_to(string(Human),
		           human_justification_tree(Tree,[])).
    term_attvars(Query, AttVars),
    maplist(del_attrs, AttVars).

Now I fear you are not entirely safe as this loads your sCASP model in the Polog user module and it will stay there because MQI handles Prolog as a stateful process. If you look at the sCASP/examples/dyncall/http.pl example, you get an HTTP server that you can send multiple sCASP programs.

Pushed a fix for that to the sCASP repo.

@ericzinda, it seems MQI cannot handle constraints in the answer. The solution is to first use term_attvars(Answer, []). if that succeeds, you are fine. Else call copy_term/3 to get a copy without attributes and the constraints as a list of goals. Now you have to think about how to hand the constraints over the MQI interface. SWISH solves this by adding an additional variable to the bindings that carries the constraints.

Is this to mean that you wanted to call the file run.py and upload it to this site but the that file type py was not allowed?

If yes then I will add py to authorized extensions in the Discourse settings for this site.

I don’t know. There are so many languages and file extensions. From a server perspective we want to avoid too big attachments and we want avoid serving malicious content. I don’t know what is considered malicious these days. Surely we do not want executable code, but some scripting code may also be considered potentially malicious.

OK

As there is a separate option for file types that can be uploaded by staff, I will add py to that list.

Thanks for the pointers. I’ll try to get a fix for this this week. I suspect I’ll just copy what SWISH does for consistency unless there is some good reason I find not to.

1 Like

Excellent, thanks, @jan. I have re-written my code as you suggested, and you are right, that alone didn’t solve the problem. I’m going to wait for @ericzinda’s fix for MQI’s handling of constraints, and try again.

I’m just trying to make sure I can run the scenario and getting it to run in the Prolog top level isn’t working for me. I started here:

?- pack_install(scasp).
... % All tests passed!
% Warning: No defined.
% Warning: Too old Prolog or no writeable candidate
% Warning: Could not install the scasp executable
true.

Then I took the Prolog code from SWISH (which does run fine for me in swish), copied it to a test.pl file and:

?- consult('test.pl').
true.

?- winner(A,B).
false.

SWISH gives me beautiful output when I run that query that hides what is really going on:

#### s(CASP) model

* rock beats scissors
* there is no evidence that o_player holds for A, and B
* there is no evidence that o_player holds for A, and C
* there is no evidence that o_throw holds for B, and rock
* there is no evidence that o_throw holds for C, and scissors
* B played in A
* C played in A
* B threw rock
* C threw scissors
* the winner of A is B
...

… so I was hoping to be able to manipulate the actual constraint terms in the top level, but I have no idea why it is not succeeding…

Edit: On the mac, SWI-Prolog (threaded, 64 bits, version 8.5.7)

Any ideas?

figured it out. that should have had a ? in front:

?- ? winner(A,B).
% s(CASP) model
{ beats(rock,scissors),      not o_throw(B,rock),       player(A,_A),              winner(A,B),
  not o_player(A,B),         not o_throw(_A,scissors),  throw(B,rock),
  not o_player(A,_A),        player(A,B),               throw(_A,scissors)
} 
1 Like

@JasonMorris, if you download this test version of MQI: mqi.pl (51.1 KB), you can install it by going to the directory where you downloaded and doing:

swipl -s mqi.pl -g "mqi:install_to_library('mqi.pl')" -t halt

You probably have to reboot swipl after. No changes to the python swiplserver client needed.

This MQI.pl will return all attributes for all the variables in each answer in a single variable that is added to each answer called Attributes.

At that point, the run.py.pl that @jan created above will run properly and return this JSON. I’ve elided the long text in the answers for Human with “…”:

[
    {
        'Attributes': [{'args': ['A', 'scasp_output', {'args': ['A'], 'functor': 'name'}], 'functor': 'put_attr'}, {'args': ['B', 'scasp_output', {'args': ['B'], 'functor': 'name'}], 'functor': 'put_attr'}], 
        'A': 'A', 
        'B': 'B', 
        'Human': '   the winner of A is B, ...\n'
    }, 
    {
        'Attributes': [{'args': ['C', 'scasp_output', {'args': ['A'], 'functor': 'name'}], 'functor': 'put_attr'}, {'args': ['D', 'scasp_output', {'args': ['B'], 'functor': 'name'}], 'functor': 'put_attr'}], 
        'A': 'C', 
        'B': 'D', 
        'Human': '   the winner of A is B, ...\n'
    }, 
    {   
        'Attributes': [{'args': ['E', 'scasp_output', {'args': ['A'], 'functor': 'name'}], 'functor': 'put_attr'}, {'args': ['F', 'scasp_output', {'args': ['B'], 'functor': 'name'}], 'functor': 'put_attr'}], 
        'A': 'E', 
        'B': 'F', 
        'Human': '   the winner of A is B, ...\n'
    }
]

@Jan:

  • I’ll change this to match the approach of SWISH and pengines if you can point me to the code where they do it
  • If you tell MQI to log STDOUT and STDERR to a file using the PrologMQI(output_file_name=...) argument, it launches and executes mqi:write_output_to_file:
write_output_to_file(File) :-
    debug(mqi(protocol), "Writing all STDOUT and STDERR to file:~w", [File]),
    open(File, write, Stream, [buffer(false)]),
    set_prolog_IO(user_input, Stream, Stream).

However, the output of a query like ? winner(A, B), which appears to be written to STDOUT, does not get put into the file. Any thoughts on why this wouldn’t work?

I’ll get a pull request for this once I’ve addressed those two questions.

1 Like

I see this is all a bit more complicated than I had in my memory. There is lib/trace.pl in SWISH that adds pseudo variables for global residual goals from e.g., CHR (global constraints) or tabling (the Well Founded Semantics Residual Program) that can be extended to capture e.g., the model and justification from s(CASP) calls. That, together with the original bindings is handled by pengines_io.pl (from the core system) that uses prolog:translate_bindings/5. This does all the stuff the normal toplevel performs: detecting cycles, find binding variables with the same value, extract constraints and cycles, etc. I think that is, especially for now, too much. You do need something to avoid errors though :frowning:

The first step is calling term_attvars/2 with [] as second argument. If that succeeds, there are no constraints and we are done. If that fails we have two options. We could decide that attributes are not part of MQI and ignore them or raise an error. That forces the user of MQI to translate the constraints to normal Prolog terms. To do that, call copy_term_nat/2 on the bindings term and process the result as normal. Alternatively, call copy_term/3 on the bindings term to end up with the same result as copy_term_nat/2 and a list of Prolog terms (goals) that represent the constraints. You can extend the protocol to transfer the constraints, but you can also extend the dictionary holding the variable bindings with an additional pseudo binding. Would that work?

You may have similar issues with cyclic terms (rational trees), e.g., calling X=f(X). You can find the solution to that in boot/toplevel.pl, factorize_bindings/2.

Yes, this is what I did above. I called the pseudo binding Attributes and it just contains the JSON serialization of the constraints from copy_term/3, without modification. I.e. they all are terms like [put_attr(...), put_attr(...)]. You’ll see that it has the constraints that remain from the SCASP example query.

It looks like pengines uses the name _residuals for the psuedo binding and binds it to a term '$residuals'(List). Should I just follow that form and have MQI return a term _residuals=$residuals([put_attr(...), put_attr(...)) in the result (I assume this is what pengines is doing: just including the output of copy_term/3 directly in $residuals)?

Here’s the predicate I put together:

handle_constraints(Answer, Final_Answer) :-
    (   term_attvars(Answer, [])
    ->  Final_Answer = Answer
    ;   findall(    Single_Answer_With_Attributes,
                    (   member(Single_Answer, Answer),
                        copy_term(Single_Answer, Single_Answer_Copy, Attributes),
                        append(['_residuals' = '$residuals'(Attributes)], Single_Answer_Copy, Single_Answer_With_Attributes)
                    ),
                    Final_Answer
        ),
        debug(mqi(query), "Constraints detected, converted: ~w to ~w", [Answer, Final_Answer])
    ).
?- put_attr(X, my_module, foo), mqi:handle_constraints([[X]], Final)
Final = [['_residuals'='$residuals'([put_attr(_A, my_module, foo)]), _A]

I’ll take a look at solving X=f(X) using factorize_bindings/2 next…

I don’t think so. There is not that much reason to make SWISH and MQI use the same conventions.

Note that there are Prolog extensions that do not provide the full answer by means of the variable bindings. CHR stores “constraints” in global variables, Tabling has the notion of delayed, which is translated into a program that carries the inconsistency if there are conflicting negations, s(CASP) has a model and justification. All these extensions have an interface to get to this data as a Prolog term. They are normally part of the toplevel interaction and that is why SWISH adds these to the answer.

Non-interactive use in Prolog also requires calling the interfaces to get access to this data if you want to reason about that part of the answer. That might be good enough for MQI.

Thanks for this, giving it a shot now.

Edit: Works perfectly. The extra information now just shows up in my interface as an extra variable, which I can work with.

I have the test file included in my project, with an extra Docker step to update the library. If you could just let me know when I don’t need that anymore, that would be great, but I’ll set an issue to remind me to check for updates and remove it.

Everyone’s help is much appreciated.

1 Like