mcmas
py-mcmas: A wrapper for the MCMAS engine and the ISPL language
API Documentation for py-mcmas. You can also return to the documentation root
Important Models
Important functions
1"""py-mcmas: A wrapper for the MCMAS engine and the ISPL language 2 3<hr style="width:100%;border-bottom:3px solid black;"> 4 5API Documentation for py-mcmas. You can also [return to the documentation root](../) 6 7### **Important Models** 8 9* [`Simulation`](#Simulation), 10* [`ISPL`](#ISPL), 11* [`Agent`](#Agent), 12* [`Environment`](#Environment) 13 14### **Important functions** 15 16* [`mcmas.engine`](#engine) 17""" 18 19from mcmas.logic import And, Eq, Equal, If, false, symbols, true # noqa 20 21# from . import ai # noqa 22from . import ctx # noqa 23from . import fmtk # noqa 24from . import logic # noqa 25from . import parser # noqa 26from .ai import Society # noqa 27from .engine import engine # noqa 28from .fmtk import Specification # noqa 29from .ispl import ISPL, Actions, Agent, DefaultAgent, Environment # noqa 30from .models import spec # noqa 31from .sim import Simulation # noqa 32 33__all__ = [ 34 # Core 35 "ISPL", 36 "Agent", 37 "Environment", 38 "Actions", 39 "DefaultAgent", 40 # ABCs 41 "Specification", 42 "Simulation", 43 "Society", 44 "engine", 45 "symbols", 46 # logic 47 "If", 48 "And", 49 "Equal", 50 "Eq", 51 # modules 52 "fmtk", 53 "ispl", 54 "spec", 55 "logic", 56 "parser", 57 "ctx", 58] # noqa
489class ISPL(Fragment): 490 """ 491 Python wrapper for ISPL specifications. 492 493 This permits partials or "fragments", i.e. the specification need not be complete and ready to run. 494 495 See the ISPL reference here: http://mattvonrocketstein.github.io/py-mcmas/isplref 496 """ 497 498 PROMPT_HINTS: typing.ClassVar = "Individual proper nouns refer to separate agents." 499 500 def __str__(self): 501 return f"<ISPL: agents={len(self.agents)} vars={len(self.environment.vars)}>" 502 503 def analyze_types(self): 504 types = super().analyze_types() 505 for agent in self.agents.values(): 506 types += agent.analyze_types() 507 return list(set(types)) 508 509 @classmethod 510 def get_trivial(kls, *args, **kwargs): 511 """ 512 ISPL(...) notation. Unlike ISPL(..) 513 514 This returns the trivial specification, with just enough 515 structure to validate and run, plus any optional 516 overrides. 517 """ 518 kwargs.update(check_slice(*args)) 519 title = kwargs.pop("title", "Minimal valid ISPL specification") 520 agents = kwargs.pop("agents", {"DefaultAgent": DefaultAgent}) 521 environment = kwargs.pop("environment", Environment(...)) 522 evaluation = kwargs.pop("evaluation", ["ticking if Environment.ticking=true"]) 523 init_states = kwargs.pop("init_states", ["Environment.ticking=true"]) 524 formulae = kwargs.pop("formulae", ["ticking"]) 525 return kls( 526 title=title, 527 environment=environment, 528 evaluation=evaluation, 529 init_states=init_states, 530 formulae=formulae, 531 agents=agents, 532 **kwargs, 533 ) 534 535 # Metadata: typing.ClassVar = fmtk.SpecificationMetadata 536 COMPARATOR: typing.ClassVar = abs 537 REQUIRED: typing.ClassVar = ["agents", "evaluation", "formulae"] 538 parser: typing.ClassVar 539 title: str = Field( 540 default="Untitled Model", 541 description="Optional title. Used as a comment at the top of the file", 542 ) 543 agents: typing.Dict[str, Agent] = Field( 544 default={}, 545 description="All agents involved in this specification. A map of {name: agent_object}", 546 ) 547 environment: Environment = Field( 548 default={}, 549 description="The environment for this specification", 550 ) 551 fairness: typing.Dict[str, typing.List[str]] = Field( 552 default={}, 553 description=( 554 "Specifies conditions that must hold infinitely often along " 555 "all execution paths, used to rule out unrealistic behaviors." 556 ), 557 ) 558 init_states: typing.InitStateType = Field( 559 default=[], 560 description="Initial global states of the system when verification begins.", 561 ) 562 evaluation: typing.EvalType = Field( 563 default=[], 564 description="Calculations, composites, aggregates that can be referenced in formulae", 565 ) 566 groups: typing.GroupsType = Field( 567 default={}, 568 description=( 569 "A map of {group_name: [member1, .. ]}" 570 "Defines collections of agents for use in group-based verification formulae." 571 ), 572 ) 573 574 formulae: typing.FormulaeType = Field( 575 description=( 576 "A list of formulas.\n\n" 577 "These will be partitioned into true/false categories, per the rest of the model" 578 ), 579 default=[], 580 ) 581 simulation: SimType = Field( 582 default=None, 583 description=( 584 "The result of simulating this specification. " 585 "Empty if simulation has never been run" 586 ), 587 ) 588 source_code: typing.Union[str, None] = Field( 589 default=None, 590 description=( 591 "Source code for this specification. " 592 "Only available if the specification was loaded from raw ISPL" 593 ), 594 ) 595 596 def __contains__(self, other): 597 """ 598 Specification algebra. 599 """ 600 if isinstance(other, Agent): 601 return other in self.agents.values() 602 else: 603 raise TypeError(f"__contains__ undefined for {[type(self), type(other)]}") 604 605 def __abs__(self) -> float: 606 """ 607 Specification algebra. 608 609 Used in __lt__ and __gt__. For ISPL specifications this 610 is the cumulative sum of formulae complexity 611 """ 612 return sum([c.score for c in self.analyze_complexity()]) 613 614 def __iadd__(self, other): 615 """ 616 Specification algebra. 617 618 This is for in-place addition, i.e. `spec+=Agent(..)` 619 """ 620 upd = self + other 621 self.update(upd.model_dump()) 622 return self 623 624 def __sub__(self, other): 625 """ 626 Specification algebra. 627 628 ISPL - agent => removes agent from this agent list, if present. 629 """ 630 # if isinstance(other, (ISPL,)): 631 # return self.model_copy(update=other.model_dump()) 632 # if isinstance(other, (Environment,)): 633 # return self.model_copy(update=dict(environment=other)) 634 if isinstance(other, (Agent,)): 635 agents = {} 636 for a in self.agents.values(): 637 if a.name != other.name: 638 agents[a.name] = a 639 return self.model_copy(update=dict(agents=agents)) 640 raise TypeError(f"Cannot add {type(self)} and {type(other)}") 641 642 def __add__(self, other): 643 """ 644 Specification algebra. 645 646 ISPL + agent => adds agent to spec ISPL + ISPL => 647 overrides first spec with values from 2nd 648 """ 649 if isinstance(other, (ISPL,)): 650 return self.model_copy(update=other.model_dump()) 651 if isinstance(other, (Environment,)): 652 return self.model_copy(update=dict(environment=other)) 653 if isinstance(other, (Agent,)): 654 agents = self.agents 655 agents.update(**{other.name: other}) 656 return self.model_copy(update=dict(agents=agents)) 657 raise TypeError(f"Cannot add {type(self)} and {type(other)}") 658 659 def model_dump_source(self): 660 """ 661 Dump the source-code for this piece of the specification. 662 """ 663 # from mcmas.engine import dict2ispl 664 return util.dict2ispl(self.model_dump()) 665 666 @util.classproperty 667 def parser(self): 668 """ 669 Shortcut for `mcmas.parser.parse` 670 """ 671 from mcmas import parser 672 673 return parser.parse 674 675 @property 676 def local_advice(self) -> list: 677 """ 678 679 """ 680 out = [] 681 for agent in self.agents: 682 agentish = self.agents[agent] 683 out += agentish.advice 684 return out 685 686 @classmethod 687 @pydantic.validate_call 688 def load_from_source( 689 kls, txt, strict: bool = False 690 ) -> typing.Dict[str, typing.Self]: 691 """ 692 Return ISPL object from given string. 693 """ 694 # from mcmas import parser 695 return kls.parser(txt) 696 697 @classmethod 698 @pydantic.validate_call 699 def load_from_ispl_file( 700 kls, 701 file: Optional[str] = None, 702 ): 703 """ 704 Return ISPL object from contents of given file. 705 """ 706 LOGGER.debug(f"ISPL.load_from_ispl_file: {file}") 707 metadata = dict(file=file) 708 if file: 709 assert os.path.exists(file), f"no such file: {file}" 710 if file.endswith("ispl"): 711 with open(file) as fhandle: 712 text = fhandle.read() 713 tmp = kls.parser(text, file=file) 714 data = tmp.model_dump(exclude="metadata") 715 return ISPL(metadata=ISPL.Metadata(**metadata), **data) 716 # return ISPL(metadata=dict(file=file, engine="bonk"), **data) 717 elif file.endswith("json"): 718 raise ValueError("refusing to work with ispl file") 719 elif file in ["-", "/dev/stdin"]: 720 metadata.update(file="<<stream>>") 721 text = sys.stdin.read().strip() 722 data = kls.parser(text).model_dump(exclude="metadata") 723 return ISPL(metadata=ISPL.Metadata(**metadata), **data) 724 else: 725 LOGGER.critical(f"could not create model from {file}") 726 raise Exception(file) 727 if text: 728 LOGGER.critical("NIY") 729 raise Exception(text) 730 731 load_from_file = load_from_ispl_file 732 733 @classmethod 734 @pydantic.validate_call 735 def load_from_json_file(kls, file: Optional[str] = None, text=None): 736 """ 737 Return ISPL object from the contents of given file. 738 739 File *must* be JSON encoded. 740 """ 741 LOGGER.critical(f"ISPL.load_from_json_file: {file}") 742 metadata = dict(file=file) 743 if file: 744 assert os.path.exists(file), f"no such file: {file}" 745 if file.endswith("json"): 746 with open(file) as fhandle: 747 data = json.loads(fhandle.read()) 748 return ISPL(metadata=ISPL.Metadata(**metadata), **data) 749 elif file.endswith("ispl"): 750 raise ValueError("refusing to work with ispl file") 751 elif file in ["-", "/dev/stdin"]: 752 metadata.update(file="<<stream>>") 753 text = sys.stdin.read().strip() 754 data = json.loads(text) 755 return ISPL( 756 metadata=ISPL.Metadata(**{**data.pop("metadata", {}), **metadata}), 757 **data, 758 ) 759 else: 760 LOGGER.critical(f"could not create model from {file}") 761 raise Exception(file) 762 763 if text: 764 LOGGER.critical("NIY") 765 raise Exception(text) 766 767 @property 768 @pydantic.validate_call 769 def validates(self) -> bool: 770 """ 771 Asks the engine directly whether this spec validates. 772 773 Note that this is ground-truth and not heuristic like the `valid` property elsewhere! 774 """ 775 return mcmas.engine.validate(model=self) 776 777 @property 778 @pydantic.validate_call 779 def analysis(self) -> spec.Analysis: 780 """ 781 Static-analysis for this ISPL specification. 782 783 Returns details about symbols and logical operators. 784 """ 785 ents = list(self.agents.values()) 786 ents += [self.environment] 787 vars = [] 788 for agent in ents: 789 if isinstance(agent, (Agent,)): 790 vars += [k.strip() for k in (agent.lobsvars or []) if k.strip()] 791 vars += [k.strip() for k in (agent.vars or []) if k.strip()] 792 vars = list(set(vars)) 793 meta = dict( 794 symbols=spec.SymbolMetadata( 795 agents=[agent for agent in self.agents], 796 vars=vars, 797 actions=list( 798 set( 799 functools.reduce( 800 operator.add, 801 [self.agents[agent].actions for agent in self.agents], 802 ) 803 ) 804 ), 805 ), 806 types=self.analyze_types(), 807 complexity=self.analyze_complexity(), 808 ) 809 meta = spec.Analysis(**meta) 810 return meta 811 812 def analyze_complexity(self) -> typing.List: 813 from mcmas.logic import complexity 814 815 return [ 816 complexity.analyzer.analyze(f.lstrip().rstrip(), index=i) 817 for i, f in enumerate(self.formulae) 818 ] 819 820 def exec(self, strict: bool = False, **kwargs) -> typing.Self: 821 """ 822 Execute this ISPL specification. 823 """ 824 required = ["init_states"] 825 self.logger.debug(f"validating: {self.source_code or self.model_dump_source()}") 826 827 # check advice before execution 828 if self.advice: 829 msg = "exec: Model has non-empty advice!" 830 LOGGER.critical(msg) 831 if strict: 832 raise RuntimeError(msg) 833 834 for k in required: 835 if not getattr(self, k): 836 err = f"Validation failed. Required key `{k}` is missing." 837 return self.model_copy( 838 update={ 839 # "source_code": src, 840 "simulation": Simulation( 841 error=err, 842 metadata=Simulation.Metadata(parsed=False, validates=False), 843 ), 844 } 845 ) 846 847 self.logger.debug("starting..") 848 sim = mcmas.engine(text=self.model_dump_source(), output_format="model") 849 out = self.model_copy( 850 update=dict( 851 source_code=self.source_code or self.model_dump_source(), 852 simulation=sim, 853 metadata=self.metadata.model_dump(), 854 ) 855 ) 856 self.logger.debug("done") 857 return out 858 859 run_sim = run_simulation = exec 860 861 def repl(self): 862 """ 863 Start a REPL shell with this object available as `spec`. 864 """ 865 result_model = self.exec() 866 return util.repl(spec=result_model)
Python wrapper for ISPL specifications.
This permits partials or "fragments", i.e. the specification need not be complete and ready to run.
See the ISPL reference here: http://mattvonrocketstein.github.io/py-mcmas/isplref
509 @classmethod 510 def get_trivial(kls, *args, **kwargs): 511 """ 512 ISPL(...) notation. Unlike ISPL(..) 513 514 This returns the trivial specification, with just enough 515 structure to validate and run, plus any optional 516 overrides. 517 """ 518 kwargs.update(check_slice(*args)) 519 title = kwargs.pop("title", "Minimal valid ISPL specification") 520 agents = kwargs.pop("agents", {"DefaultAgent": DefaultAgent}) 521 environment = kwargs.pop("environment", Environment(...)) 522 evaluation = kwargs.pop("evaluation", ["ticking if Environment.ticking=true"]) 523 init_states = kwargs.pop("init_states", ["Environment.ticking=true"]) 524 formulae = kwargs.pop("formulae", ["ticking"]) 525 return kls( 526 title=title, 527 environment=environment, 528 evaluation=evaluation, 529 init_states=init_states, 530 formulae=formulae, 531 agents=agents, 532 **kwargs, 533 )
ISPL(...) notation. Unlike ISPL(..)
This returns the trivial specification, with just enough structure to validate and run, plus any optional overrides.
659 def model_dump_source(self): 660 """ 661 Dump the source-code for this piece of the specification. 662 """ 663 # from mcmas.engine import dict2ispl 664 return util.dict2ispl(self.model_dump())
Dump the source-code for this piece of the specification.
686 @classmethod 687 @pydantic.validate_call 688 def load_from_source( 689 kls, txt, strict: bool = False 690 ) -> typing.Dict[str, typing.Self]: 691 """ 692 Return ISPL object from given string. 693 """ 694 # from mcmas import parser 695 return kls.parser(txt)
Return ISPL object from given string.
697 @classmethod 698 @pydantic.validate_call 699 def load_from_ispl_file( 700 kls, 701 file: Optional[str] = None, 702 ): 703 """ 704 Return ISPL object from contents of given file. 705 """ 706 LOGGER.debug(f"ISPL.load_from_ispl_file: {file}") 707 metadata = dict(file=file) 708 if file: 709 assert os.path.exists(file), f"no such file: {file}" 710 if file.endswith("ispl"): 711 with open(file) as fhandle: 712 text = fhandle.read() 713 tmp = kls.parser(text, file=file) 714 data = tmp.model_dump(exclude="metadata") 715 return ISPL(metadata=ISPL.Metadata(**metadata), **data) 716 # return ISPL(metadata=dict(file=file, engine="bonk"), **data) 717 elif file.endswith("json"): 718 raise ValueError("refusing to work with ispl file") 719 elif file in ["-", "/dev/stdin"]: 720 metadata.update(file="<<stream>>") 721 text = sys.stdin.read().strip() 722 data = kls.parser(text).model_dump(exclude="metadata") 723 return ISPL(metadata=ISPL.Metadata(**metadata), **data) 724 else: 725 LOGGER.critical(f"could not create model from {file}") 726 raise Exception(file) 727 if text: 728 LOGGER.critical("NIY") 729 raise Exception(text)
Return ISPL object from contents of given file.
697 @classmethod 698 @pydantic.validate_call 699 def load_from_ispl_file( 700 kls, 701 file: Optional[str] = None, 702 ): 703 """ 704 Return ISPL object from contents of given file. 705 """ 706 LOGGER.debug(f"ISPL.load_from_ispl_file: {file}") 707 metadata = dict(file=file) 708 if file: 709 assert os.path.exists(file), f"no such file: {file}" 710 if file.endswith("ispl"): 711 with open(file) as fhandle: 712 text = fhandle.read() 713 tmp = kls.parser(text, file=file) 714 data = tmp.model_dump(exclude="metadata") 715 return ISPL(metadata=ISPL.Metadata(**metadata), **data) 716 # return ISPL(metadata=dict(file=file, engine="bonk"), **data) 717 elif file.endswith("json"): 718 raise ValueError("refusing to work with ispl file") 719 elif file in ["-", "/dev/stdin"]: 720 metadata.update(file="<<stream>>") 721 text = sys.stdin.read().strip() 722 data = kls.parser(text).model_dump(exclude="metadata") 723 return ISPL(metadata=ISPL.Metadata(**metadata), **data) 724 else: 725 LOGGER.critical(f"could not create model from {file}") 726 raise Exception(file) 727 if text: 728 LOGGER.critical("NIY") 729 raise Exception(text)
Return ISPL object from contents of given file.
733 @classmethod 734 @pydantic.validate_call 735 def load_from_json_file(kls, file: Optional[str] = None, text=None): 736 """ 737 Return ISPL object from the contents of given file. 738 739 File *must* be JSON encoded. 740 """ 741 LOGGER.critical(f"ISPL.load_from_json_file: {file}") 742 metadata = dict(file=file) 743 if file: 744 assert os.path.exists(file), f"no such file: {file}" 745 if file.endswith("json"): 746 with open(file) as fhandle: 747 data = json.loads(fhandle.read()) 748 return ISPL(metadata=ISPL.Metadata(**metadata), **data) 749 elif file.endswith("ispl"): 750 raise ValueError("refusing to work with ispl file") 751 elif file in ["-", "/dev/stdin"]: 752 metadata.update(file="<<stream>>") 753 text = sys.stdin.read().strip() 754 data = json.loads(text) 755 return ISPL( 756 metadata=ISPL.Metadata(**{**data.pop("metadata", {}), **metadata}), 757 **data, 758 ) 759 else: 760 LOGGER.critical(f"could not create model from {file}") 761 raise Exception(file) 762 763 if text: 764 LOGGER.critical("NIY") 765 raise Exception(text)
Return ISPL object from the contents of given file.
File must be JSON encoded.
767 @property 768 @pydantic.validate_call 769 def validates(self) -> bool: 770 """ 771 Asks the engine directly whether this spec validates. 772 773 Note that this is ground-truth and not heuristic like the `valid` property elsewhere! 774 """ 775 return mcmas.engine.validate(model=self)
Asks the engine directly whether this spec validates.
Note that this is ground-truth and not heuristic like the valid property elsewhere!
777 @property 778 @pydantic.validate_call 779 def analysis(self) -> spec.Analysis: 780 """ 781 Static-analysis for this ISPL specification. 782 783 Returns details about symbols and logical operators. 784 """ 785 ents = list(self.agents.values()) 786 ents += [self.environment] 787 vars = [] 788 for agent in ents: 789 if isinstance(agent, (Agent,)): 790 vars += [k.strip() for k in (agent.lobsvars or []) if k.strip()] 791 vars += [k.strip() for k in (agent.vars or []) if k.strip()] 792 vars = list(set(vars)) 793 meta = dict( 794 symbols=spec.SymbolMetadata( 795 agents=[agent for agent in self.agents], 796 vars=vars, 797 actions=list( 798 set( 799 functools.reduce( 800 operator.add, 801 [self.agents[agent].actions for agent in self.agents], 802 ) 803 ) 804 ), 805 ), 806 types=self.analyze_types(), 807 complexity=self.analyze_complexity(), 808 ) 809 meta = spec.Analysis(**meta) 810 return meta
Static-analysis for this ISPL specification.
Returns details about symbols and logical operators.
820 def exec(self, strict: bool = False, **kwargs) -> typing.Self: 821 """ 822 Execute this ISPL specification. 823 """ 824 required = ["init_states"] 825 self.logger.debug(f"validating: {self.source_code or self.model_dump_source()}") 826 827 # check advice before execution 828 if self.advice: 829 msg = "exec: Model has non-empty advice!" 830 LOGGER.critical(msg) 831 if strict: 832 raise RuntimeError(msg) 833 834 for k in required: 835 if not getattr(self, k): 836 err = f"Validation failed. Required key `{k}` is missing." 837 return self.model_copy( 838 update={ 839 # "source_code": src, 840 "simulation": Simulation( 841 error=err, 842 metadata=Simulation.Metadata(parsed=False, validates=False), 843 ), 844 } 845 ) 846 847 self.logger.debug("starting..") 848 sim = mcmas.engine(text=self.model_dump_source(), output_format="model") 849 out = self.model_copy( 850 update=dict( 851 source_code=self.source_code or self.model_dump_source(), 852 simulation=sim, 853 metadata=self.metadata.model_dump(), 854 ) 855 ) 856 self.logger.debug("done") 857 return out
Execute this ISPL specification.
861 def repl(self): 862 """ 863 Start a REPL shell with this object available as `spec`. 864 """ 865 result_model = self.exec() 866 return util.repl(spec=result_model)
Start a REPL shell with this object available as spec.
Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].
820 def exec(self, strict: bool = False, **kwargs) -> typing.Self: 821 """ 822 Execute this ISPL specification. 823 """ 824 required = ["init_states"] 825 self.logger.debug(f"validating: {self.source_code or self.model_dump_source()}") 826 827 # check advice before execution 828 if self.advice: 829 msg = "exec: Model has non-empty advice!" 830 LOGGER.critical(msg) 831 if strict: 832 raise RuntimeError(msg) 833 834 for k in required: 835 if not getattr(self, k): 836 err = f"Validation failed. Required key `{k}` is missing." 837 return self.model_copy( 838 update={ 839 # "source_code": src, 840 "simulation": Simulation( 841 error=err, 842 metadata=Simulation.Metadata(parsed=False, validates=False), 843 ), 844 } 845 ) 846 847 self.logger.debug("starting..") 848 sim = mcmas.engine(text=self.model_dump_source(), output_format="model") 849 out = self.model_copy( 850 update=dict( 851 source_code=self.source_code or self.model_dump_source(), 852 simulation=sim, 853 metadata=self.metadata.model_dump(), 854 ) 855 ) 856 self.logger.debug("done") 857 return out
Execute this ISPL specification.
820 def exec(self, strict: bool = False, **kwargs) -> typing.Self: 821 """ 822 Execute this ISPL specification. 823 """ 824 required = ["init_states"] 825 self.logger.debug(f"validating: {self.source_code or self.model_dump_source()}") 826 827 # check advice before execution 828 if self.advice: 829 msg = "exec: Model has non-empty advice!" 830 LOGGER.critical(msg) 831 if strict: 832 raise RuntimeError(msg) 833 834 for k in required: 835 if not getattr(self, k): 836 err = f"Validation failed. Required key `{k}` is missing." 837 return self.model_copy( 838 update={ 839 # "source_code": src, 840 "simulation": Simulation( 841 error=err, 842 metadata=Simulation.Metadata(parsed=False, validates=False), 843 ), 844 } 845 ) 846 847 self.logger.debug("starting..") 848 sim = mcmas.engine(text=self.model_dump_source(), output_format="model") 849 out = self.model_copy( 850 update=dict( 851 source_code=self.source_code or self.model_dump_source(), 852 simulation=sim, 853 metadata=self.metadata.model_dump(), 854 ) 855 ) 856 self.logger.debug("done") 857 return out
Execute this ISPL specification.
288class Agent(Fragment): 289 """ 290 Python wrapper for ISPL Agents. 291 292 See also the relevant [ISPL reference](http://mattvonrocketstein.github.io/py-mcmas/isplref/#agent) 293 """ 294 295 COMPARATOR: typing.ClassVar = len 296 DefaultAgent: typing.ClassVar 297 REQUIRED: typing.ClassVar = ["protocol", "evolution", "actions"] 298 parser: typing.ClassVar 299 300 name: str = Field( 301 default="player", 302 description=("Name of this agent"), 303 ) 304 actions: typing.ActionsType = ActionsField 305 evolution: typing.EvolType = EvolutionField 306 obsvars: typing.ObsVarsType = ObsvarsField 307 protocol: typing.ProtocolType = ProtocolField 308 vars: typing.VarsType = VarsField 309 310 lobsvars: typing.LobsvarsType = Field( 311 description="", 312 default=[], 313 ) 314 red_states: typing.List[str] = Field( 315 description="", 316 default=[], 317 ) 318 319 __len__ = Environment.__len__ 320 321 @util.classproperty 322 def DefaultAgent(kls): 323 """ 324 Returns the trivial agent. 325 """ 326 return kls(...) 327 328 @classmethod 329 def _load_from_pydantic_ai_agent(kls, pagent, **extra) -> typing.Self: 330 """ 331 Create ISPL agent from the given pydantic agent. 332 """ 333 kls.logger.info(f"_load_from_pydantic_ai_agent: {pagent}") 334 from mcmas import util 335 336 name = pagent.name or f"Agent_{id(pagent)}" 337 actions = list(pagent._function_toolset.tools.keys()) 338 tools = list(pagent._function_toolset.tools.values()) 339 tool = tools[0] 340 if len(tools) > 1: 341 kls.logger.warning(f"pydantic agent `{name}` has multiple tools!") 342 kls.logger.warning(f"using just the first one: {tool}") 343 # naive conversion from function signature to ISPL types. 344 vars = util.fxn_sig.as_ispl_types(tool.function) 345 vars.pop("ctx", None) 346 return Agent( 347 name=name, 348 actions=actions, 349 vars=vars, 350 metadata=dict( 351 parser=f"{kls.__module__}.{kls.__name__}._load_from_pydantic_ai_agent", 352 file=inspect.getfile(pagent.__class__), 353 ), 354 **extra, 355 ).model_completion() 356 357 def analyze_symbols(self) -> typing.List[str]: 358 """ 359 Returns symbol-related metadata including agent-names, 360 actions, variables, and type info. 361 """ 362 return spec.SymbolMetadata( 363 agents=[self.name], 364 actions=self.actions, 365 vars=[k.strip() for k in list(self.lobsvars) + list(self.vars)], 366 types=self.analyze_types(), 367 ) 368 369 @property 370 @pydantic.validate_call 371 def analysis(self) -> spec.Analysis: 372 """ 373 Static-analysis for this Agent specification. 374 375 Returns details about symbols and logical operators. 376 """ 377 return spec.Analysis( 378 symbols=self.analyze_symbols(), 379 types=self.analyze_types(), 380 complexity=self.analyze_complexity(), 381 ) 382 # out.actions = [getattr(symbols, x) for x in sorted(list(set(out.actions)))] 383 # out.vars = [getattr(symbols, x) for x in sorted(list(set(out.vars)))] 384 # out.agents = [getattr(symbols, x) for x in sorted(list(set(out.agents)))] 385 # return meta 386 387 def __pow__(self, other: float = 0.1) -> typing.Self: 388 """ 389 390 """ 391 from mcmas import ai 392 393 return ai.model_mutation(obj=self, model_settings=dict(top_p=other)) 394 395 def __invert__(self) -> typing.Self: 396 """ 397 Trigger completion for this Agent. 398 399 This is NOT backed by an LLM; see instead `mcmas.ai.agent_completion`. 400 """ 401 if not self.advice: 402 err = f"{self} is already valid, returning it instead of completing" 403 LOGGER.warning(err) 404 return self 405 else: 406 tmp = self 407 trivial = DefaultAgent 408 defaults = dict( 409 protocol=trivial.protocol, 410 vars=trivial.vars, 411 evolution=trivial.evolution, 412 ) 413 for x in ["protocol", "vars", "evolution"]: 414 if getattr(self, x, None): 415 pass 416 else: 417 LOGGER.warning(f"could not find required {x}") 418 tmp = tmp.model_copy(update={x: defaults[x]}) 419 return tmp.model_copy( 420 update=dict(actions=list(set(tmp.actions + trivial.actions))) 421 ) 422 423 model_completion = __invert__ 424 425 @classmethod 426 def get_trivial(kls, *args, **kwargs): 427 """ 428 Smallest legal agent. 429 """ 430 kwargs.update(check_slice(*args)) 431 return kls( 432 name="trivial", 433 vars=dict(ticking="boolean"), 434 actions=["tick"], 435 protocol=["Other : {tick}"], 436 evolution=["ticking=true if Action=tick;"], 437 ) 438 439 def model_dump_source(self): 440 """ 441 Dump the source-code for this piece of the specification. 442 """ 443 from mcmas import rendering 444 445 return rendering.get_template("Agent.j2").render( 446 name=self.name, agent=self.model_dump() 447 ) 448 449 @util.classproperty 450 def parser(self) -> typing.Callable: 451 """ 452 Return an appropriate parser for this spec-fragment. 453 """ 454 from mcmas import parser 455 456 return parser.extract_agents 457 458 @classmethod 459 @pydantic.validate_call 460 def load_from_source(kls, txt, strict: bool = False) -> typing.Self: 461 """ 462 Load ISPL agent from string. 463 """ 464 agents = kls.parser(txt) 465 agents.pop("Environment", None) 466 if len(agents) != 1: 467 LOGGER.critical("load_from_source: more than 1 agent! returning first..") 468 return Agent(**list(agents.values())[0]) 469 470 @property 471 def local_advice(self) -> list: 472 return []
Python wrapper for ISPL Agents.
See also the relevant ISPL reference
357 def analyze_symbols(self) -> typing.List[str]: 358 """ 359 Returns symbol-related metadata including agent-names, 360 actions, variables, and type info. 361 """ 362 return spec.SymbolMetadata( 363 agents=[self.name], 364 actions=self.actions, 365 vars=[k.strip() for k in list(self.lobsvars) + list(self.vars)], 366 types=self.analyze_types(), 367 )
Returns symbol-related metadata including agent-names, actions, variables, and type info.
369 @property 370 @pydantic.validate_call 371 def analysis(self) -> spec.Analysis: 372 """ 373 Static-analysis for this Agent specification. 374 375 Returns details about symbols and logical operators. 376 """ 377 return spec.Analysis( 378 symbols=self.analyze_symbols(), 379 types=self.analyze_types(), 380 complexity=self.analyze_complexity(), 381 ) 382 # out.actions = [getattr(symbols, x) for x in sorted(list(set(out.actions)))] 383 # out.vars = [getattr(symbols, x) for x in sorted(list(set(out.vars)))] 384 # out.agents = [getattr(symbols, x) for x in sorted(list(set(out.agents)))] 385 # return meta
Static-analysis for this Agent specification.
Returns details about symbols and logical operators.
395 def __invert__(self) -> typing.Self: 396 """ 397 Trigger completion for this Agent. 398 399 This is NOT backed by an LLM; see instead `mcmas.ai.agent_completion`. 400 """ 401 if not self.advice: 402 err = f"{self} is already valid, returning it instead of completing" 403 LOGGER.warning(err) 404 return self 405 else: 406 tmp = self 407 trivial = DefaultAgent 408 defaults = dict( 409 protocol=trivial.protocol, 410 vars=trivial.vars, 411 evolution=trivial.evolution, 412 ) 413 for x in ["protocol", "vars", "evolution"]: 414 if getattr(self, x, None): 415 pass 416 else: 417 LOGGER.warning(f"could not find required {x}") 418 tmp = tmp.model_copy(update={x: defaults[x]}) 419 return tmp.model_copy( 420 update=dict(actions=list(set(tmp.actions + trivial.actions))) 421 )
Trigger completion for this Agent.
This is NOT backed by an LLM; see instead mcmas.ai.agent_completion.
425 @classmethod 426 def get_trivial(kls, *args, **kwargs): 427 """ 428 Smallest legal agent. 429 """ 430 kwargs.update(check_slice(*args)) 431 return kls( 432 name="trivial", 433 vars=dict(ticking="boolean"), 434 actions=["tick"], 435 protocol=["Other : {tick}"], 436 evolution=["ticking=true if Action=tick;"], 437 )
Smallest legal agent.
439 def model_dump_source(self): 440 """ 441 Dump the source-code for this piece of the specification. 442 """ 443 from mcmas import rendering 444 445 return rendering.get_template("Agent.j2").render( 446 name=self.name, agent=self.model_dump() 447 )
Dump the source-code for this piece of the specification.
458 @classmethod 459 @pydantic.validate_call 460 def load_from_source(kls, txt, strict: bool = False) -> typing.Self: 461 """ 462 Load ISPL agent from string. 463 """ 464 agents = kls.parser(txt) 465 agents.pop("Environment", None) 466 if len(agents) != 1: 467 LOGGER.critical("load_from_source: more than 1 agent! returning first..") 468 return Agent(**list(agents.values())[0])
Load ISPL agent from string.
223class Environment(Fragment): 224 """ 225 Python wrapper for ISPL Environments. Environments model the 226 shared information and boundary conditions that all other 227 agents can observe. 228 229 See also the relevant [ISPL reference](http://mattvonrocketstein.github.io/py-mcmas/isplref/#environment) 230 """ 231 232 REQUIRED: typing.ClassVar = ["protocol", "actions"] 233 DefaultEnvironment: typing.ClassVar 234 actions: typing.ActionsType = ActionsField 235 vars: typing.VarsType = VarsField 236 obsvars: typing.ObsVarsType = ObsvarsField 237 evolution: typing.EvolType = EvolutionField 238 protocol: typing.ProtocolType = ProtocolField 239 240 def __len__(self): 241 """ 242 Specification Algebra. 243 244 The length of an agent is the number of items in the 245 description length 246 """ 247 return sum( 248 map( 249 len, 250 [ 251 self.actions, 252 self.evolution, 253 self.obsvars, 254 self.protocol, 255 self.vars, 256 getattr(self, "lobsvars", []), 257 getattr(self, "red_states", []), 258 ], 259 ) 260 ) 261 262 @util.classproperty 263 def DefaultEnvironment(kls): 264 return kls(...) 265 266 @classmethod 267 def get_trivial(kls, *args, **kwargs): 268 kwargs.update(check_slice(*args)) 269 actions = kwargs.pop("actions", [symbols.tick]) 270 protocol = kwargs.pop("protocol", dict(Other=[symbols.tick])) 271 vars = kwargs.pop("vars", dict(ticking="boolean")) 272 return kls(vars=vars, actions=actions, protocol=protocol, **kwargs) 273 274 @classmethod 275 @pydantic.validate_call 276 def load_from_source(kls, txt: str) -> typing.Self: 277 """ 278 Creates an Environment from string. 279 """ 280 from mcmas import parser 281 282 agents = parser.extract_agents(txt) 283 env = agents.pop("Environment", None) 284 assert env is not None 285 return Environment(**env)
Python wrapper for ISPL Environments. Environments model the shared information and boundary conditions that all other agents can observe.
See also the relevant ISPL reference
266 @classmethod 267 def get_trivial(kls, *args, **kwargs): 268 kwargs.update(check_slice(*args)) 269 actions = kwargs.pop("actions", [symbols.tick]) 270 protocol = kwargs.pop("protocol", dict(Other=[symbols.tick])) 271 vars = kwargs.pop("vars", dict(ticking="boolean")) 272 return kls(vars=vars, actions=actions, protocol=protocol, **kwargs)
Subclassers must implement this.
Returns the trivial fragment for this type.
274 @classmethod 275 @pydantic.validate_call 276 def load_from_source(kls, txt: str) -> typing.Self: 277 """ 278 Creates an Environment from string. 279 """ 280 from mcmas import parser 281 282 agents = parser.extract_agents(txt) 283 env = agents.pop("Environment", None) 284 assert env is not None 285 return Environment(**env)
Creates an Environment from string.
154class Specification(SpecificationFragment): 155 """ 156 Pydantic models for a Specification. 157 """
Pydantic models for a Specification.
165class Simulation(SimBase): 166 """ 167 A Simulation object is the result of having run a spec. 168 169 In practice a Simulation in `py-mcmas` is always an ISPL program 170 running on an MCMAS engine, but see `SimBase` for something more 171 generic. 172 """
A Simulation object is the result of having run a spec.
In practice a Simulation in py-mcmas is always an ISPL program
running on an MCMAS engine, but see SimBase for something more
generic.
222class Society: 223 """ 224 Agent-discovery for a few different ecosystems / frameworks. 225 Supported frameworks are { pydantic_ai | openai | camelai }. 226 227 Instantiate a Society with the framework module, or a string 228 version of the module name. This auto-detects all agents that 229 are defined / instantiated for this runtime. 230 231 These class-properties are also available as shortcuts: 232 * `Society.pydantic` 233 * `Society.openai` 234 """ 235 236 def __getitem__(self, other): 237 return self.get_spec(other) 238 239 def get_spec(self, other): 240 """ 241 242 """ 243 tmp = [x for x in self] 244 if isinstance(other, (str,)): 245 agent = None 246 for agent in tmp: 247 if getattr(agent, "name", None) == tmp: 248 break 249 if other in tmp: 250 agent = other 251 return agent_completion(agent=agent, framework=self.module) 252 253 def __init__(self, module): 254 """ 255 Instantiate a Society with the framework module, or a 256 string version of the module name. 257 258 This auto-detects all agents that are defined / 259 instantiated for this runtime. 260 """ 261 name = getattr(module, "__name__", module) 262 assert name in ["pydantic_ai", "openai", "agents"] 263 self._module = module 264 self._society = [] 265 if name == "pydantic_ai": 266 try: 267 import pydantic_ai 268 except ImportError as exc: 269 LOGGER.critical( 270 "`pydantic_ai` module not available. " 271 f"pip install py-mcmas[ai] or openai-agents {exc}" 272 ) 273 else: 274 self._society = util.find_instances(pydantic_ai.Agent) 275 elif name in ["openai", "agents"]: 276 try: 277 from agents import Agent 278 except ImportError: 279 LOGGER.critical( 280 "`agents` module not available. " 281 "pip install py-mcmas[ai] or openai-agents" 282 ) 283 else: 284 self._society = util.find_instances(Agent) 285 286 else: 287 raise TypeError(f"niy {[type(module), module]}") 288 289 def __iter__(self): 290 return iter(self._society) 291 292 @util.classproperty 293 def pydantic(kls) -> typing.List: 294 """ 295 Finds all the pydantic agents. 296 """ 297 return kls("pydantic_ai") 298 299 @util.classproperty 300 def openai(kls) -> typing.List: 301 """ 302 Finds all the openai agents. 303 """ 304 return kls("openai")
Agent-discovery for a few different ecosystems / frameworks. Supported frameworks are { pydantic_ai | openai | camelai }.
Instantiate a Society with the framework module, or a string version of the module name. This auto-detects all agents that are defined / instantiated for this runtime.
These class-properties are also available as shortcuts:
* Society.pydantic
* Society.openai
253 def __init__(self, module): 254 """ 255 Instantiate a Society with the framework module, or a 256 string version of the module name. 257 258 This auto-detects all agents that are defined / 259 instantiated for this runtime. 260 """ 261 name = getattr(module, "__name__", module) 262 assert name in ["pydantic_ai", "openai", "agents"] 263 self._module = module 264 self._society = [] 265 if name == "pydantic_ai": 266 try: 267 import pydantic_ai 268 except ImportError as exc: 269 LOGGER.critical( 270 "`pydantic_ai` module not available. " 271 f"pip install py-mcmas[ai] or openai-agents {exc}" 272 ) 273 else: 274 self._society = util.find_instances(pydantic_ai.Agent) 275 elif name in ["openai", "agents"]: 276 try: 277 from agents import Agent 278 except ImportError: 279 LOGGER.critical( 280 "`agents` module not available. " 281 "pip install py-mcmas[ai] or openai-agents" 282 ) 283 else: 284 self._society = util.find_instances(Agent) 285 286 else: 287 raise TypeError(f"niy {[type(module), module]}")
Instantiate a Society with the framework module, or a string version of the module name.
This auto-detects all agents that are defined / instantiated for this runtime.
239 def get_spec(self, other): 240 """ 241 242 """ 243 tmp = [x for x in self] 244 if isinstance(other, (str,)): 245 agent = None 246 for agent in tmp: 247 if getattr(agent, "name", None) == tmp: 248 break 249 if other in tmp: 250 agent = other 251 return agent_completion(agent=agent, framework=self.module)
395@pydantic.validate_call 396def engine( 397 fname: Union[str, PosixPath] = "", 398 text: str = "", 399 model=None, 400 data: dict = {}, 401 file: Union[str, PosixPath] = "", 402 **kwargs, 403) -> Union[Dict, str]: 404 """ 405 Runs the engine on either a filename, a block of ISPL text, 406 or an ISPL- Specification object. 407 """ 408 if text: 409 LOGGER.debug("------------") 410 LOGGER.debug(text) 411 LOGGER.debug("------------") 412 with tempfile.NamedTemporaryFile( 413 mode="w+", suffix=".ispl", delete=True, dir="." 414 ) as temp_file: 415 temp_file.write(text) 416 temp_file.flush() 417 result = mcmas(fname=temp_file.name, **kwargs) 418 # raise Exception(result.metadata) 419 return result 420 # elif file and file=='/dev/stdin': 421 # import sys 422 # raise Exception(sys.stdin.read()) 423 # return engine(text=sys.stdin.read()) 424 elif model: 425 return engine(data=model.model_dump(), **kwargs) 426 elif fname or file: 427 return mcmas(fname=fname or file, **kwargs) 428 elif data: 429 return engine(text=util.dict2ispl(data), **kwargs) 430 else: 431 err = "No input, expected one of {text|model|data}" 432 raise Exception(err + f"\n{[fname,text,model,data,file]}")
Runs the engine on either a filename, a block of ISPL text, or an ISPL- Specification object.