← All ivy-nodes
IVYXSTUDIO · IVY NODE
S

Social: lay a reply out for its platform

ivy.node.reply-plan · v0.1.0

ivyx✓

Reads an IVYX Social reply request (the conversation, its platform, the approved text, the connection) and says which responder runs. No model, no network.

#social#reply#plan#ivyx-social

Inputs

FieldTypeDescription
conversation_idrequiredstringThe conversation's id (ig:<comment>, li:<comment>, x:<post>, yt:<comment>).
platformrequiredstringNot declared
post_keystringNot declared
authorstringThe LinkedIn page's URN, for a LinkedIn reply.
connectionNot declaredThe platform's connection (connection.<id>), handed on by the agent's input.
textrequiredstringThe approved reply.

Outputs

FieldTypeDescription
has_instagrambooleanNot declared
has_linkedinbooleanNot declared
has_xbooleanNot declared
has_youtubebooleanNot declared
comment_idstringNot declared
reply_tostringNot declared
textstringNot declared
okrequiredbooleanNot declared
reasonstringNot declared

Source

python

# Input preparation
inp = __ivy_ctx__["nodes"][__ivy_node_id__]["input"]

# Compute
# Shared by every social node (built into each by build.mjs): dates, blocks, merging.
import datetime, json, re, statistics

def day(s):
    s = str(s or "")[:10]
    return s if re.fullmatch(r"\d{4}-\d{2}-\d{2}", s) else None

def now_iso():
    return datetime.datetime.utcnow().replace(microsecond=0).isoformat() + "Z"

def ms_of(s):
    if s is None or s == "":
        return None
    if isinstance(s, (int, float)):
        return int(s * 1000) if s < 1e12 else int(s)
    t = str(s).replace("Z", "+00:00")
    t = re.sub(r"([+-]\d{2})(\d{2})$", r"\1:\2", t)
    try:
        return int(datetime.datetime.fromisoformat(t).timestamp() * 1000)
    except ValueError:
        return None

def rows_of(block):
    """A {fields, rows} block as dicts."""
    if not isinstance(block, dict) or not block.get("fields"):
        return []
    f = block["fields"]
    return [dict(zip(f, r)) for r in block.get("rows") or []]

def block_of(dicts, first):
    fields = list(first)
    for d in dicts:
        for k in d:
            if k not in fields:
                fields.append(k)
    return {"fields": fields, "rows": [[d.get(k) for k in fields] for d in dicts]}

def listed(v):
    return [x for x in (v if isinstance(v, list) else [v] if v else []) if x is not None]

def text_of(v, limit=4000):
    s = " ".join(str(v).split()) if isinstance(v, (str, int, float)) else ""
    return s[:limit]


class ReplyPlan:
    "Social: lay a reply out for its platform"

    def run(self, inp):
        out = {}
        # One reply (a conversation and the approved text) laid out per platform, so the agent's if-branches run the right responder.
        platform = str(inp.get("platform") or "").lower()
        cid = str(inp.get("conversation_id") or "")
        text = str(inp.get("text") or "").strip()
        has = lambda p: platform == p and bool(inp.get("connection")) and bool(text)
        out["has_instagram"] = has("instagram")
        out["has_linkedin"] = has("linkedin") and bool(inp.get("post_key")) and bool(inp.get("author"))
        out["has_x"] = has("x")
        out["has_youtube"] = has("youtube")
        out["comment_id"] = cid.split(":", 1)[1] if ":" in cid else cid
        out["reply_to"] = out["comment_id"]
        out["text"] = text
        out["ok"] = any(out[k] for k in ("has_instagram", "has_linkedin", "has_x", "has_youtube"))
        out["reason"] = "" if out["ok"] else f"No responder for {platform or 'this platform'} with what was given."
        return out


result = ReplyPlan().run(inp)

# Output collection (the runner reads it here)
out = __ivy_ctx__["nodes"][__ivy_node_id__]["output"]
out.update(result)

Tests

Requires: python:3.9

  • ig

    An Instagram comment with a connection and a text runs the Instagram responder on the comment's own id.

  • no-connection

    A YouTube reply without a connection runs no responder and says so.

  • linkedin-author

    A LinkedIn reply needs the post and the author beside the connection.