For the at-least variant, it seems you can write down this problem as propositional logic and convert your constraints into Conjunctive Normal Form, which gives you the constraints you need only using the edge-variables.
Given sets $A$ and $B$ and variables $\forall i\in A, j \in B: x_{i,j}$, the constraints can be written as
$$\bigwedge_{i,j \in A} \bigvee_{k \in B} (x_{ik} \wedge x_{jk})$$
which are not the constraints of the language of Integer Programming. However, these constraints can be converted into the Conjunctive Normal Form to obtain constraints that are easily mapped to IP constraints.
I made a short Python script that uses the boolean algebra module of Sympy to automate this process.
from sympy.core import Symbol
from sympy.logic.boolalg import *
# Define the sizes of sets A and B
a_size = 3
b_size = 2
# Define sets A and B
A = [i for i in range(a_size)]
B = [i for i in range(b_size)]
# Build a dictionary for the variables
X = {i : {j : Symbol('x{}_{}'.format(i,j)) for j in B} for i in A}
# Construct a logic expression capturing all constraints
expr = True
for i in A:
for j in A:
if i != j:
subExpr = False
for k in B:
subSubExpr = And(X[i][k],X[j][k])
subExpr = Or(subExpr, subSubExpr)
expr = And(expr, subExpr)
print('The logic expression is:')
print(expr)
# Convert the logic expression to CNF
cnf_expr = to_cnf(expr)
print('In CNF the logic expression is:')
print(cnf_expr)
# Iterate over the 'And' terms of the CNF expression
for conjunctive_term in cnf_expr.args:
# Extract variable names of the terms
varnames = [str(a) for a in conjunctive_term.args]
# Print the IP-constraint corresponding to the conjunctive term
lhs = ' + '.join(varnames)
print(lhs + ' >= 1')
Applied to a case where $|A| = 3$ and $|B| = 2$, this gives us the logical constraint
$$((x_{0,0} \wedge x_{1,0}) \vee (x_{0,1} \wedge x_{1,1})) \wedge ((x_{0,0} \wedge x_{2,0}) \vee (x_{0,1} \wedge x_{2,1})) \wedge ((x_{1,0} \wedge x_{2,0}) \vee (x_{1,1} \wedge x_{2,1}))$$
converted to CNF this constraint becomes
$$(x_{0,0} \vee x_{0,1}) \wedge (x_{0,0} \vee x_{1,1}) \wedge (x_{0,0} \vee x_{2,1}) \wedge (x_{0,1} \vee x_{1,0}) \wedge (x_{0,1} \vee x_{2,0}) \wedge (x_{1,0} \vee x_{1,1}) \wedge (x_{1,0} \vee x_{2,1}) \wedge (x_{1,1} \vee x_{2,0}) \wedge (x_{2,0} \vee x_{2,1})$$
And converted to linear constraints, this gives us
$$
\begin{align*}
x_{0,0} + x_{0,1} &>= 1 \\
x_{0,0} + x_{1,1} &>= 1 \\
x_{0,0} + x_{2,1} &>= 1 \\
x_{0,1} + x_{1,0} &>= 1 \\
x_{0,1} + x_{2,0} &>= 1 \\
x_{1,0} + x_{1,1} &>= 1 \\
x_{1,0} + x_{2,1} &>= 1 \\
x_{1,1} + x_{2,0} &>= 1 \\
x_{2,0} + x_{2,1} &>= 1
\end{align*}
$$
Note that the number of constraints generated by this approach grows quite rapidly: for $|A|=3$ and $|B|$ is $3, 4, 5, 6$ the numbers of constraints generated by this script seem to be $21, 45, 93, 189$. The Online Encyclopedia of Integer Sequences seems to contain some information on this sequence which suggest some binomial-related growth, but I did not analyze this in more detail. It may thus be necessary to figure out the actual structure of the generated constraints and use a cutting plane method to solve larger instances in practice.
However, I do think does avoid the need to generate the triplet variables for the at-least variant.